Merge pull request #3795 from matrix-org/valere/invisible_crypto/verification_pin

Crypto | Add support for verified identities change detection.
This commit is contained in:
Richard van der Hoff
2024-08-08 11:10:47 +01:00
committed by GitHub
7 changed files with 1109 additions and 39 deletions

View File

@@ -28,6 +28,7 @@ Abl = "Abl"
Som = "Som"
Ba = "Ba"
Yur = "Yur" # as found in crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs
TYE = "TYE" # as found in testing/matrix-sdk-test/src/test_json/keys_query_sets.rs
[files]
extend-exclude = [

View File

@@ -39,7 +39,7 @@ use crate::{
Result as StoreResult, Store, StoreCache, UserKeyQueryResult,
},
types::{CrossSigningKey, DeviceKeys, MasterPubkey, SelfSigningPubkey, UserSigningPubkey},
CryptoStoreError, LocalTrust, SignatureError,
CryptoStoreError, LocalTrust, OwnUserIdentity, SignatureError, UserIdentities,
};
enum DeviceChange {
@@ -85,6 +85,13 @@ struct KeysQueryRequestDetails {
request_ids: HashSet<OwnedTransactionId>,
}
// Helper type to handle key query response
struct KeySetInfo {
user_id: OwnedUserId,
master_key: MasterPubkey,
self_signing: SelfSigningPubkey,
}
impl IdentityManager {
const MAX_KEY_QUERY_USERS: usize = 250;
@@ -444,6 +451,7 @@ impl IdentityManager {
async fn handle_changed_identity(
&self,
response: &KeysQueryResponse,
maybe_verified_own_identity: Option<&OwnUserIdentity>,
master_key: MasterPubkey,
self_signing: SelfSigningPubkey,
i: UserIdentityData,
@@ -461,7 +469,12 @@ impl IdentityManager {
}
}
UserIdentityData::Other(mut identity) => {
let has_changed = identity.update(master_key, self_signing)?;
let has_changed = identity.update(
master_key,
self_signing,
maybe_verified_own_identity.map(|o| o.user_signing_key()),
)?;
if has_changed {
Ok(IdentityUpdateResult::Updated(identity.into()))
} else {
@@ -502,11 +515,13 @@ impl IdentityManager {
async fn handle_new_identity(
&self,
response: &KeysQueryResponse,
maybe_verified_own_identity: Option<&OwnUserIdentity>,
master_key: MasterPubkey,
self_signing: SelfSigningPubkey,
changed_private_identity: &mut Option<PrivateCrossSigningIdentity>,
) -> Result<UserIdentityData, SignatureError> {
if master_key.user_id() == self.user_id() {
// Own identity
let user_signing = self.get_user_signing_key_from_response(response)?;
let identity = OwnUserIdentityData::new(master_key, self_signing, user_signing)?;
*changed_private_identity = self.check_private_identity(&identity).await;
@@ -514,6 +529,13 @@ impl IdentityManager {
} else {
// First time seen, create the identity. The current MSK will be pinned.
let identity = OtherUserIdentityData::new(master_key, self_signing)?;
let is_verified = maybe_verified_own_identity.map_or(false, |own_user_identity| {
own_user_identity.is_identity_signed(&identity).is_ok()
});
if is_verified {
identity.mark_as_previously_verified();
}
Ok(identity.into())
}
}
@@ -616,10 +638,9 @@ impl IdentityManager {
/// * `changed_identity` - Output parameter: Unchanged if the identity is
/// that of another user. If it is our own, set to `None` or `Some`
/// depending on whether our stored private identity needs updating.
/// * `user_id` - The user id of the user whose identity is being processed.
/// * `master_key` - The public master cross-signing key for this user from
/// the `/keys/query` response.
/// * `self_signing` - The public self-signing key from the `/keys/query`
/// * `maybe_verified_own_identity` - Own verified identity if any to check
/// verification status of updated identity.
/// * `key_set_info` - The identity info as returned by the `/keys/query`
/// response.
#[instrument(skip_all, fields(user_id))]
async fn update_or_create_identity(
@@ -627,17 +648,18 @@ impl IdentityManager {
response: &KeysQueryResponse,
changes: &mut IdentityChanges,
changed_private_identity: &mut Option<PrivateCrossSigningIdentity>,
user_id: &UserId,
master_key: MasterPubkey,
self_signing: SelfSigningPubkey,
maybe_verified_own_identity: Option<&OwnUserIdentity>,
key_set_info: KeySetInfo,
) -> StoreResult<()> {
let KeySetInfo { user_id, master_key, self_signing } = key_set_info;
if master_key.user_id() != user_id || self_signing.user_id() != user_id {
warn!(?user_id, "User ID mismatch in one of the cross signing keys");
} else if let Some(i) = self.store.get_user_identity(user_id).await? {
} else if let Some(i) = self.store.get_user_identity(&user_id).await? {
// an identity we knew about before, which is being updated
match self
.handle_changed_identity(
response,
maybe_verified_own_identity,
master_key,
self_signing,
i,
@@ -660,7 +682,13 @@ impl IdentityManager {
} else {
// an identity we did not know about before
match self
.handle_new_identity(response, master_key, self_signing, changed_private_identity)
.handle_new_identity(
response,
maybe_verified_own_identity,
master_key,
self_signing,
changed_private_identity,
)
.await
{
Ok(identity) => {
@@ -698,6 +726,15 @@ impl IdentityManager {
let mut changes = IdentityChanges::default();
let mut changed_identity = None;
// We want to check if the updated/new other identities are trusted by us or
// not. This is based on the current verified state of the own identity.
let maybe_own_verified_identity = self
.store
.get_identity(self.user_id())
.await?
.and_then(UserIdentities::own)
.filter(|own| own.is_verified());
for (user_id, master_key) in &response.master_keys {
// Get the master and self-signing key for each identity; those are required for
// every user identity type. If we don't have those we skip over.
@@ -707,13 +744,14 @@ impl IdentityManager {
continue;
};
let key_set_info = KeySetInfo { user_id: user_id.clone(), master_key, self_signing };
self.update_or_create_identity(
response,
&mut changes,
&mut changed_identity,
user_id,
master_key,
self_signing,
maybe_own_verified_identity.as_ref(),
key_set_info,
)
.await?;
}
@@ -1353,6 +1391,7 @@ pub(crate) mod tests {
use crate::{
identities::manager::testing::{other_key_query_cross_signed, own_key_query},
olm::PrivateCrossSigningIdentity,
CrossSigningKeyExport, OlmMachine,
};
fn key_query_with_failures() -> KeysQueryResponse {
@@ -2023,4 +2062,269 @@ pub(crate) mod tests {
assert!(!other_identity.has_pin_violation());
}
// Set up a machine do initial own key query and import cross-signing secret to
// make the current session verified.
async fn common_verified_identity_changes_machine_setup() -> OlmMachine {
use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet;
let machine = OlmMachine::new(DataSet::own_id(), device_id!("LOCAL")).await;
let keys_query = DataSet::own_keys_query_response_1();
let txn_id = TransactionId::new();
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
machine
.import_cross_signing_keys(CrossSigningKeyExport {
master_key: DataSet::MASTER_KEY_PRIVATE_EXPORT.to_owned().into(),
self_signing_key: DataSet::SELF_SIGNING_KEY_PRIVATE_EXPORT.to_owned().into(),
user_signing_key: DataSet::USER_SIGNING_KEY_PRIVATE_EXPORT.to_owned().into(),
})
.await
.unwrap();
machine
}
#[async_test]
async fn test_manager_verified_latch_setup_on_new_identities() {
use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet;
let machine = common_verified_identity_changes_machine_setup().await;
// ######
// First test: Assert that the latch is properly set on new identities
// ######
let keys_query = DataSet::bob_keys_query_response_signed();
let txn_id = TransactionId::new();
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
let own_identity =
machine.get_identity(DataSet::own_id(), None).await.unwrap().unwrap().own().unwrap();
// For sanity check that own identity is trusted
assert!(own_identity.is_verified());
let bob_identity =
machine.get_identity(DataSet::bob_id(), None).await.unwrap().unwrap().other().unwrap();
// The verified latch should be true
assert!(bob_identity.was_previously_verified());
// And bob is verified
assert!(bob_identity.is_verified());
// ######
// Second test: Assert that the local latch stays on if the identity is rotated
// ######
let keys_query = DataSet::bob_keys_query_response_rotated();
let txn_id = TransactionId::new();
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
let bob_identity =
machine.get_identity(DataSet::bob_id(), None).await.unwrap().unwrap().other().unwrap();
// Bob is not verified anymore
assert!(!bob_identity.is_verified());
// The verified latch should still be true
assert!(bob_identity.was_previously_verified());
// Bob device_2 is self-signed even if there is this verification latch
// violation
let bob_device = machine
.get_device(DataSet::bob_id(), DataSet::bob_device_2_id(), None)
.await
.unwrap()
.unwrap();
assert!(bob_identity.is_device_signed(&bob_device).is_ok());
// there is also a pin violation
assert!(bob_identity.has_pin_violation());
// Fixing the pin violation won't fix the verification latch violation
bob_identity.pin_current_master_key().await.unwrap();
assert!(!bob_identity.has_pin_violation());
let has_latch_violation =
bob_identity.was_previously_verified() && !bob_identity.is_verified();
assert!(has_latch_violation);
}
#[async_test]
async fn test_manager_verified_identity_changes_setup_on_updated_identities() {
use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet;
let machine = common_verified_identity_changes_machine_setup().await;
// ######
// Get the Carol identity for the first time
// ######
let keys_query = DataSet::carol_keys_query_response_unsigned();
let txn_id = TransactionId::new();
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
let carol_identity = machine
.get_identity(DataSet::carol_id(), None)
.await
.unwrap()
.unwrap()
.other()
.unwrap();
// The identity is not verified
assert!(!carol_identity.is_verified());
// The verified latch is off
assert!(!carol_identity.was_previously_verified());
// Carol is verified, likely from another session. Ensure the latch is updated
// when the key query response is processed
let keys_query = DataSet::carol_keys_query_response_signed();
let txn_id = TransactionId::new();
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
let carol_identity = machine
.get_identity(DataSet::carol_id(), None)
.await
.unwrap()
.unwrap()
.other()
.unwrap();
assert!(carol_identity.is_verified());
// This should have updated the latch
assert!(carol_identity.was_previously_verified());
// It is the same identity, it's just signed now so no pin violation
assert!(!carol_identity.has_pin_violation());
}
// Set up a machine do initial own key query.
// The cross signing secrets are not yet uploaded.
// Then query keys for carol and bob (both signed by own identity)
async fn common_verified_identity_changes_own_trust_change_machine_setup() -> OlmMachine {
use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet;
// Start on a non-verified session
let machine = OlmMachine::new(DataSet::own_id(), device_id!("LOCAL")).await;
let keys_query = DataSet::own_keys_query_response_1();
let txn_id = TransactionId::new();
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
// For sanity check that own identity is not trusted
let own_identity =
machine.get_identity(DataSet::own_id(), None).await.unwrap().unwrap().own().unwrap();
assert!(!own_identity.is_verified());
let keys_query = DataSet::own_keys_query_response_1();
let txn_id = TransactionId::new();
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
// Get Bob and Carol already signed
let keys_query = DataSet::bob_keys_query_response_signed();
let txn_id = TransactionId::new();
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
let keys_query = DataSet::carol_keys_query_response_signed();
let txn_id = TransactionId::new();
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
machine.update_tracked_users(vec![DataSet::bob_id(), DataSet::carol_id()]).await.unwrap();
machine
}
#[async_test]
async fn test_manager_verified_identity_changes_setup_on_own_identity_trust_change() {
use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet;
let machine = common_verified_identity_changes_own_trust_change_machine_setup().await;
let own_identity =
machine.get_identity(DataSet::own_id(), None).await.unwrap().unwrap().own().unwrap();
let bob_identity =
machine.get_identity(DataSet::bob_id(), None).await.unwrap().unwrap().other().unwrap();
// Carol is verified by our identity but our own identity is not yet trusted
assert!(own_identity.is_identity_signed(&bob_identity).is_ok());
assert!(!bob_identity.was_previously_verified());
let carol_identity = machine
.get_identity(DataSet::carol_id(), None)
.await
.unwrap()
.unwrap()
.other()
.unwrap();
// Carol is verified by our identity but our own identity is not yet trusted
assert!(own_identity.is_identity_signed(&carol_identity).is_ok());
assert!(!carol_identity.was_previously_verified());
// Marking our own identity as trusted should update the existing identities
let _ = own_identity.verify().await;
let own_identity =
machine.get_identity(DataSet::own_id(), None).await.unwrap().unwrap().own().unwrap();
assert!(own_identity.is_verified());
let carol_identity = machine
.get_identity(DataSet::carol_id(), None)
.await
.unwrap()
.unwrap()
.other()
.unwrap();
assert!(carol_identity.is_verified());
// The latch should be set now
assert!(carol_identity.was_previously_verified());
let bob_identity =
machine.get_identity(DataSet::bob_id(), None).await.unwrap().unwrap().other().unwrap();
assert!(bob_identity.is_verified());
// The latch should be set now
assert!(bob_identity.was_previously_verified());
}
#[async_test]
async fn test_manager_verified_identity_change_setup_on_import_secrets() {
use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet;
let machine = common_verified_identity_changes_own_trust_change_machine_setup().await;
let own_identity =
machine.get_identity(DataSet::own_id(), None).await.unwrap().unwrap().own().unwrap();
let bob_identity =
machine.get_identity(DataSet::bob_id(), None).await.unwrap().unwrap().other().unwrap();
// Carol is verified by our identity but our own identity is not yet trusted
assert!(own_identity.is_identity_signed(&bob_identity).is_ok());
assert!(!bob_identity.was_previously_verified());
let carol_identity = machine
.get_identity(DataSet::carol_id(), None)
.await
.unwrap()
.unwrap()
.other()
.unwrap();
// Carol is verified by our identity but our own identity is not yet trusted
assert!(own_identity.is_identity_signed(&carol_identity).is_ok());
assert!(!carol_identity.was_previously_verified());
// Marking our own identity as trusted should update the existing identities
machine
.import_cross_signing_keys(CrossSigningKeyExport {
master_key: DataSet::MASTER_KEY_PRIVATE_EXPORT.to_owned().into(),
self_signing_key: DataSet::SELF_SIGNING_KEY_PRIVATE_EXPORT.to_owned().into(),
user_signing_key: DataSet::USER_SIGNING_KEY_PRIVATE_EXPORT.to_owned().into(),
})
.await
.unwrap();
let own_identity =
machine.get_identity(DataSet::own_id(), None).await.unwrap().unwrap().own().unwrap();
assert!(own_identity.is_verified());
let carol_identity = machine
.get_identity(DataSet::carol_id(), None)
.await
.unwrap()
.unwrap()
.other()
.unwrap();
assert!(carol_identity.is_verified());
// The latch should be set now
assert!(carol_identity.was_previously_verified());
let bob_identity =
machine.get_identity(DataSet::bob_id(), None).await.unwrap().unwrap().other().unwrap();
assert!(bob_identity.is_verified());
// The latch should be set now
assert!(bob_identity.was_previously_verified());
}
}

View File

@@ -332,6 +332,48 @@ impl UserIdentity {
// higher priority than pinning.
self.inner.has_pin_violation()
}
/// Remove the requirement for this identity to be verified.
pub async fn withdraw_verification(&self) -> Result<(), CryptoStoreError> {
self.inner.withdraw_verification();
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(())
}
// Test helper
#[cfg(test)]
pub async fn mark_as_previously_verified(&self) -> Result<(), CryptoStoreError> {
self.inner.mark_as_previously_verified();
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(())
}
/// Was this identity verified since initial observation and is not anymore?
///
/// Such a violation should be reported to the local user by the
/// application, and resolved by
///
/// - Verifying the new identity with [`UserIdentity::request_verification`]
/// - Or by withdrawing the verification requirement
/// [`UserIdentity::withdraw_verification`].
pub fn has_verification_violation(&self) -> bool {
if !self.inner.was_previously_verified() {
// If that identity has never been verified it cannot be in violation.
return false;
};
!self.is_verified()
}
}
/// Enum over the different user identity types we can have.
@@ -415,7 +457,8 @@ impl UserIdentityData {
/// signatures can be checked with this identity.
///
/// This struct also contains the currently pinned user identity (public master
/// key) for that user.
/// key) for that user and a local flag that serves as a latch to remember if an
/// identity was verified once.
///
/// The first time a cryptographic user identity is seen for a given user, it
/// will be associated with that user ("pinned"). Future interactions
@@ -424,6 +467,12 @@ impl UserIdentityData {
///
/// The user can explicitly pin the new identity to allow for legitimate
/// identity changes (for example, in case of key material or device loss).
///
/// As soon as the cryptographic identity is verified (i.e. signed by our own
/// trusted identity), a flag is set to remember it (`previously_verified`).
/// Future interactions will expect this user to stay verified, in case of
/// violation the user should be notified with a blocking warning when sending a
/// message.
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(try_from = "OtherUserIdentityDataSerializer", into = "OtherUserIdentityDataSerializer")]
pub struct OtherUserIdentityData {
@@ -431,6 +480,11 @@ pub struct OtherUserIdentityData {
pub(crate) master_key: Arc<MasterPubkey>,
self_signing_key: Arc<SelfSigningPubkey>,
pinned_master_key: Arc<RwLock<MasterPubkey>>,
/// This tracks whether this olm machine has already seen this user as
/// verified. To use it in the future to detect cases where the user has
/// become unverified for any reason. This can be reset using
/// [`OtherUserIdentityData::withdraw_verification()`].
previously_verified: Arc<AtomicBool>,
}
/// Intermediate struct to help serialize OtherUserIdentityData and support
@@ -460,6 +514,15 @@ struct OtherUserIdentityDataSerializerV1 {
pinned_master_key: MasterPubkey,
}
#[derive(Debug, Deserialize, Serialize)]
struct OtherUserIdentityDataSerializerV2 {
user_id: OwnedUserId,
master_key: MasterPubkey,
self_signing_key: SelfSigningPubkey,
pinned_master_key: MasterPubkey,
previously_verified: bool,
}
impl TryFrom<OtherUserIdentityDataSerializer> for OtherUserIdentityData {
type Error = serde_json::Error;
fn try_from(
@@ -475,6 +538,7 @@ impl TryFrom<OtherUserIdentityDataSerializer> for OtherUserIdentityData {
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)),
previously_verified: Arc::new(false.into()),
})
}
Some(v) if v == "1" => {
@@ -484,6 +548,19 @@ impl TryFrom<OtherUserIdentityDataSerializer> for OtherUserIdentityData {
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)),
// Put it to false. There will be a migration to mark all users as dirty, so we
// will receive an update for the identity that will correctly set up the value.
previously_verified: Arc::new(false.into()),
})
}
Some(v) if v == "2" => {
let v2: OtherUserIdentityDataSerializerV2 = serde_json::from_value(value.other)?;
Ok(OtherUserIdentityData {
user_id: v2.user_id,
master_key: Arc::new(v2.master_key.clone()),
self_signing_key: Arc::new(v2.self_signing_key),
pinned_master_key: Arc::new(RwLock::new(v2.pinned_master_key)),
previously_verified: Arc::new(v2.previously_verified.into()),
})
}
_ => Err(serde::de::Error::custom(format!("Unsupported Version {:?}", value.version))),
@@ -493,15 +570,16 @@ impl TryFrom<OtherUserIdentityDataSerializer> for OtherUserIdentityData {
impl From<OtherUserIdentityData> for OtherUserIdentityDataSerializer {
fn from(value: OtherUserIdentityData) -> Self {
let v1 = OtherUserIdentityDataSerializerV1 {
let v2 = OtherUserIdentityDataSerializerV2 {
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(),
previously_verified: value.previously_verified.load(Ordering::SeqCst),
};
OtherUserIdentityDataSerializer {
version: Some("1".to_owned()),
other: serde_json::to_value(v1).unwrap(),
version: Some("2".to_owned()),
other: serde_json::to_value(v2).unwrap(),
}
}
}
@@ -549,6 +627,7 @@ impl OtherUserIdentityData {
master_key: master_key.clone().into(),
self_signing_key: self_signing_key.into(),
pinned_master_key: RwLock::new(master_key).into(),
previously_verified: Arc::new(false.into()),
})
}
@@ -563,6 +642,7 @@ impl OtherUserIdentityData {
master_key: Arc::new(master_key.clone()),
self_signing_key,
pinned_master_key: Arc::new(RwLock::new(master_key.clone())),
previously_verified: Arc::new(false.into()),
}
}
@@ -587,6 +667,29 @@ impl OtherUserIdentityData {
*m = self.master_key.as_ref().clone()
}
/// Remember that this identity used to be verified at some point.
pub(crate) fn mark_as_previously_verified(&self) {
self.previously_verified.store(true, Ordering::SeqCst)
}
/// True if we verified this identity (with any own identity, at any
/// point).
///
/// To pass this latch back to false, one must call
/// [`OtherUserIdentityData::withdraw_verification()`].
pub fn was_previously_verified(&self) -> bool {
self.previously_verified.load(Ordering::SeqCst)
}
/// Remove the requirement for this identity to be verified.
///
/// If an identity was previously verified and is not anymore it will be
/// reported to the user. In order to remove this notice users have to
/// verify again or to withdraw the verification requirement.
pub fn withdraw_verification(&self) {
self.previously_verified.store(false, Ordering::SeqCst)
}
/// 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
@@ -609,6 +712,9 @@ impl OtherUserIdentityData {
///
/// * `self_signing_key` - The new self signing key of user identity.
///
/// * `maybe_verified_own_user_signing_key` - Our own user_signing_key if it
/// is verified to check the identity trust status after update.
///
/// Returns a `SignatureError` if we failed to update the identity.
/// Otherwise, returns `true` if there was a change to the identity and
/// `false` if the identity is unchanged.
@@ -616,6 +722,7 @@ impl OtherUserIdentityData {
&mut self,
master_key: MasterPubkey,
self_signing_key: SelfSigningPubkey,
maybe_verified_own_user_signing_key: Option<&UserSigningPubkey>,
) -> Result<bool, SignatureError> {
master_key.verify_subkey(&self_signing_key)?;
@@ -625,11 +732,21 @@ impl OtherUserIdentityData {
// (see `has_pin_violation()`).
let pinned_master_key = self.pinned_master_key.read().unwrap().clone();
// Check if the new master_key is signed by our own **verified**
// user_signing_key. If the identity was verified we remember it.
let updated_is_verified = maybe_verified_own_user_signing_key
.map_or(false, |own_user_signing_key| {
own_user_signing_key.verify_master_key(&master_key).is_ok()
});
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(),
previously_verified: Arc::new(
(self.was_previously_verified() || updated_is_verified).into(),
),
};
let changed = new != *self;
@@ -961,19 +1078,17 @@ pub(crate) mod tests {
use super::{
testing::{device, get_other_identity, get_own_identity},
OwnUserIdentityData, UserIdentityData,
OtherUserIdentityDataSerializerV2, OwnUserIdentityData, UserIdentityData,
};
use crate::{
identities::{
manager::testing::own_key_query,
user::{OtherUserIdentityDataSerializer, OtherUserIdentityDataSerializerV1},
Device,
manager::testing::own_key_query, user::OtherUserIdentityDataSerializer, Device,
},
olm::{Account, PrivateCrossSigningIdentity},
store::{CryptoStoreWrapper, MemoryStore},
types::{CrossSigningKey, MasterPubkey, SelfSigningPubkey, Signatures, UserSigningPubkey},
verification::VerificationMachine,
OlmMachine, OtherUserIdentityData,
CrossSigningKeyExport, OlmMachine, OtherUserIdentityData,
};
#[test]
@@ -1076,12 +1191,12 @@ pub(crate) mod tests {
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 _: OtherUserIdentityDataSerializerV2 =
serde_json::from_value(value.clone()).expect("Should deserialize as version 2");
let with_serializer: OtherUserIdentityDataSerializer =
serde_json::from_value(value).unwrap();
assert_eq!("1", with_serializer.version.unwrap());
assert_eq!("2", with_serializer.version.unwrap());
}
#[test]
@@ -1315,4 +1430,83 @@ pub(crate) mod tests {
// But there is still a pin violation
assert!(other_identity.inner.has_pin_violation());
}
#[async_test]
async fn resolve_identity_verification_violation_with_withdraw() {
use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet;
let machine = OlmMachine::new(DataSet::own_id(), device_id!("LOCAL")).await;
let keys_query = DataSet::own_keys_query_response_1();
let txn_id = TransactionId::new();
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
machine
.import_cross_signing_keys(CrossSigningKeyExport {
master_key: DataSet::MASTER_KEY_PRIVATE_EXPORT.to_owned().into(),
self_signing_key: DataSet::SELF_SIGNING_KEY_PRIVATE_EXPORT.to_owned().into(),
user_signing_key: DataSet::USER_SIGNING_KEY_PRIVATE_EXPORT.to_owned().into(),
})
.await
.unwrap();
let keys_query = DataSet::bob_keys_query_response_rotated();
let txn_id = TransactionId::new();
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
let bob_identity =
machine.get_identity(DataSet::bob_id(), None).await.unwrap().unwrap().other().unwrap();
// For testing purpose mark it as previously verified
bob_identity.mark_as_previously_verified().await.unwrap();
assert!(bob_identity.has_verification_violation());
// withdraw
bob_identity.withdraw_verification().await.unwrap();
let bob_identity =
machine.get_identity(DataSet::bob_id(), None).await.unwrap().unwrap().other().unwrap();
assert!(!bob_identity.has_verification_violation());
}
#[async_test]
async fn reset_own_keys_creates_verification_violation() {
use test_json::keys_query_sets::PreviouslyVerifiedTestData as DataSet;
let machine = OlmMachine::new(DataSet::own_id(), device_id!("LOCAL")).await;
let keys_query = DataSet::own_keys_query_response_1();
let txn_id = TransactionId::new();
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
machine
.import_cross_signing_keys(CrossSigningKeyExport {
master_key: DataSet::MASTER_KEY_PRIVATE_EXPORT.to_owned().into(),
self_signing_key: DataSet::SELF_SIGNING_KEY_PRIVATE_EXPORT.to_owned().into(),
user_signing_key: DataSet::USER_SIGNING_KEY_PRIVATE_EXPORT.to_owned().into(),
})
.await
.unwrap();
let keys_query = DataSet::bob_keys_query_response_signed();
let txn_id = TransactionId::new();
machine.mark_request_as_sent(&txn_id, &keys_query).await.unwrap();
let bob_identity =
machine.get_identity(DataSet::bob_id(), None).await.unwrap().unwrap().other().unwrap();
// For testing purpose mark it as previously verified
bob_identity.mark_as_previously_verified().await.unwrap();
assert!(!bob_identity.has_verification_violation());
let _ = machine.bootstrap_cross_signing(true).await.unwrap();
let bob_identity =
machine.get_identity(DataSet::bob_id(), None).await.unwrap().unwrap().other().unwrap();
assert!(bob_identity.has_verification_violation());
}
}

View File

@@ -147,6 +147,7 @@ impl std::fmt::Debug for OlmMachine {
impl OlmMachine {
const CURRENT_GENERATION_STORE_KEY: &'static str = "generation-counter";
const HAS_MIGRATED_VERIFICATION_LATCH: &'static str = "HAS_MIGRATED_VERIFICATION_LATCH";
/// Create a new memory based OlmMachine.
///
@@ -358,9 +359,40 @@ impl OlmMachine {
let identity = Arc::new(Mutex::new(identity));
let store = Arc::new(CryptoStoreWrapper::new(user_id, device_id, store));
// FIXME: We might want in the future a more generic high-level data migration
// mechanism (at the store wrapper layer).
Self::migration_post_verified_latch_support(&store).await?;
Ok(OlmMachine::new_helper(device_id, store, static_account, identity, maybe_backup_key))
}
// The sdk now support verified identity change detection.
// This introduces a new local flag (`verified_latch` on
// `OtherUserIdentityData`). In order to ensure that this flag is up-to-date and
// for the sake of simplicity we force a re-download of tracked users by marking
// them as dirty.
//
// pub(crate) visibility for testing.
pub(crate) async fn migration_post_verified_latch_support(
store: &CryptoStoreWrapper,
) -> Result<(), CryptoStoreError> {
let maybe_migrate_for_identity_verified_latch =
store.get_custom_value(Self::HAS_MIGRATED_VERIFICATION_LATCH).await?.is_none();
if maybe_migrate_for_identity_verified_latch {
// We want to mark all tracked users as dirty to ensure the verified latch is
// set up correctly.
let tracked_user = store.load_tracked_users().await?;
let mut store_updates = Vec::with_capacity(tracked_user.len());
tracked_user.iter().for_each(|tu| {
store_updates.push((tu.user_id.as_ref(), true));
});
store.save_tracked_users(&store_updates).await?;
store.set_custom_value(Self::HAS_MIGRATED_VERIFICATION_LATCH, vec![0]).await?
}
Ok(())
}
/// Get the crypto store associated with this `OlmMachine` instance.
pub fn store(&self) -> &Store {
&self.inner.store
@@ -805,11 +837,11 @@ impl OlmMachine {
}
#[instrument(
skip_all,
// This function is only ever called by add_room_key via
// handle_decrypted_to_device_event, so sender, sender_key, and algorithm are
// already recorded.
fields(room_id = ?content.room_id, session_id)
skip_all,
// This function is only ever called by add_room_key via
// handle_decrypted_to_device_event, so sender, sender_key, and algorithm are
// already recorded.
fields(room_id = ? content.room_id, session_id)
)]
async fn handle_key(
&self,
@@ -5009,4 +5041,39 @@ pub(crate) mod tests {
.unwrap();
assert_matches!(thread_encryption_result, UnsignedDecryptionResult::Decrypted(_));
}
#[async_test]
async fn test_verified_latch_migration() {
let store = MemoryStore::new();
let account = vodozemac::olm::Account::new();
// put some tracked users
let bob_id = user_id!("@bob:localhost");
let carol_id = user_id!("@carol:localhost");
// Mark them as not dirty
let to_track_not_dirty = vec![(bob_id, false), (carol_id, false)];
store.save_tracked_users(&to_track_not_dirty).await.unwrap();
let alice = OlmMachine::with_store(user_id(), alice_device_id(), store, Some(account))
.await
.unwrap();
// A migration should have occurred and all users should be marked as dirty
alice.store().load_tracked_users().await.unwrap().iter().for_each(|tu| {
assert!(tu.dirty);
});
// Ensure it does so only once
alice.store().save_tracked_users(&to_track_not_dirty).await.unwrap();
OlmMachine::migration_post_verified_latch_support(alice.store().crypto_store().as_ref())
.await
.unwrap();
// Migration already done, so user should not be marked as dirty
alice.store().load_tracked_users().await.unwrap().iter().for_each(|tu| {
assert!(!tu.dirty);
});
}
}

View File

@@ -6,13 +6,14 @@ use matrix_sdk_common::store_locks::CrossProcessStoreLock;
use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId};
use tokio::sync::{broadcast, Mutex};
use tokio_stream::wrappers::{errors::BroadcastStreamRecvError, BroadcastStream};
use tracing::warn;
use tracing::{debug, trace, warn};
use super::{caches::SessionStore, DeviceChanges, IdentityChanges, LockableCryptoStore};
use crate::{
olm::InboundGroupSession,
store::{self, Changes, DynCryptoStore, IntoCryptoStore, RoomKeyInfo, RoomKeyWithheldInfo},
GossippedSecret, OwnUserIdentityData, Session,
store,
store::{Changes, DynCryptoStore, IntoCryptoStore, RoomKeyInfo, RoomKeyWithheldInfo},
CryptoStoreError, GossippedSecret, OwnUserIdentityData, Session, UserIdentityData,
};
/// A wrapper for crypto store implementations that adds update notifiers.
@@ -92,6 +93,17 @@ impl CryptoStoreWrapper {
})
.collect();
// If our own identity verified status changes we need to do some checks on
// other identities. So remember the verification status before
// processing the changes
let own_identity_was_verified_before_change = self
.store
.get_user_identity(self.user_id.as_ref())
.await?
.as_ref()
.and_then(|i| i.own())
.map_or(false, |own| own.is_verified());
let secrets = changes.secrets.to_owned();
let devices = changes.devices.to_owned();
let identities = changes.identities.to_owned();
@@ -131,10 +143,71 @@ impl CryptoStoreWrapper {
// Mapping the devices and user identities from the read-only variant to one's
// that contain side-effects requires our own identity. This is
// guaranteed to be up-to-date since we just persisted it.
let own_identity =
let maybe_own_identity =
self.store.get_user_identity(&self.user_id).await?.and_then(|i| i.into_own());
let _ = self.identities_broadcaster.send((own_identity, identities, devices));
// If our identity was not verified before the change and is now, that means
// this could impact the verification chain of other known
// identities.
if let Some(own_identity_after) = maybe_own_identity.as_ref() {
// Only do this if our identity is passing from not verified to verified,
// the previously_verified can only change in that case.
if !own_identity_was_verified_before_change && own_identity_after.is_verified() {
debug!("Own identity is now verified, check all known identities for verification status changes");
// We need to review all the other identities to see if they are verified now
// and mark them as such
self.check_all_identities_and_update_was_previously_verified_flag_if_needed(
own_identity_after,
)
.await?;
}
}
let _ = self.identities_broadcaster.send((maybe_own_identity, identities, devices));
}
Ok(())
}
async fn check_all_identities_and_update_was_previously_verified_flag_if_needed(
&self,
own_identity_after: &OwnUserIdentityData,
) -> Result<(), CryptoStoreError> {
let tracked_users = self.store.load_tracked_users().await?;
let mut updated_identities: Vec<UserIdentityData> = Default::default();
for tracked_user in tracked_users {
if let Some(other_identity) = self
.store
.get_user_identity(tracked_user.user_id.as_ref())
.await?
.as_ref()
.and_then(|i| i.other())
{
if !other_identity.was_previously_verified()
&& own_identity_after.is_identity_signed(other_identity).is_ok()
{
trace!(?tracked_user.user_id, "Marking set verified_latch to true.");
other_identity.mark_as_previously_verified();
updated_identities.push(other_identity.clone().into());
}
}
}
if !updated_identities.is_empty() {
let identity_changes =
IdentityChanges { changed: updated_identities, ..Default::default() };
self.store
.save_changes(Changes {
identities: identity_changes.clone(),
..Default::default()
})
.await?;
let _ = self.identities_broadcaster.send((
Some(own_identity_after.clone()),
identity_changes,
DeviceChanges::default(),
));
}
Ok(())

View File

@@ -83,7 +83,7 @@ pub struct MemoryStore {
tracked_users: StdRwLock<HashMap<OwnedUserId, TrackedUser>>,
olm_hashes: StdRwLock<HashMap<String, HashSet<String>>>,
devices: DeviceStore,
identities: StdRwLock<HashMap<OwnedUserId, UserIdentityData>>,
identities: StdRwLock<HashMap<OwnedUserId, String>>,
outgoing_key_requests: StdRwLock<HashMap<OwnedTransactionId, GossipRequest>>,
key_requests_by_info: StdRwLock<HashMap<String, OwnedTransactionId>>,
direct_withheld_info: StdRwLock<HashMap<OwnedRoomId, HashMap<String, RoomKeyWithheldEvent>>>,
@@ -231,7 +231,10 @@ impl CryptoStore for MemoryStore {
{
let mut identities = self.identities.write().unwrap();
for identity in changes.identities.new.into_iter().chain(changes.identities.changed) {
identities.insert(identity.user_id().to_owned(), identity.clone());
identities.insert(
identity.user_id().to_owned(),
serde_json::to_string(&identity).unwrap(),
);
}
}
@@ -481,7 +484,14 @@ impl CryptoStore for MemoryStore {
}
async fn get_user_identity(&self, user_id: &UserId) -> Result<Option<UserIdentityData>> {
Ok(self.identities.read().unwrap().get(user_id).cloned())
let serialized = self.identities.read().unwrap().get(user_id).cloned();
match serialized {
None => Ok(None),
Some(serialized) => {
let id: UserIdentityData = serde_json::from_str(serialized.as_str()).unwrap();
Ok(Some(id))
}
}
}
async fn is_message_known(&self, message_hash: &crate::olm::OlmMessageHash) -> Result<bool> {

View File

@@ -667,3 +667,424 @@ impl IdentityChangeDataSet {
.expect("Can't parse the `/keys/upload` response")
}
}
pub struct PreviouslyVerifiedTestData {}
#[allow(dead_code)]
impl PreviouslyVerifiedTestData {
pub const MASTER_KEY_PRIVATE_EXPORT: &'static str =
"bSa0nVTocZArMzL7OLmeFUIVF4ycp64rrkVMgqOYg6Y";
pub const SELF_SIGNING_KEY_PRIVATE_EXPORT: &'static str =
"MQ7b3MDXvOEMDvIOWkuH1XCNUyqBLqbdd1bT00p8HPU";
pub const USER_SIGNING_KEY_PRIVATE_EXPORT: &'static str =
"v77s+TlT5/NbcQym2B7Rwf20HOAhyInF2p1ZUYDPtow";
pub fn own_id() -> &'static UserId {
user_id!("@alice:localhost")
}
pub fn bob_id() -> &'static UserId {
user_id!("@bob:localhost")
}
pub fn carol_id() -> &'static UserId {
user_id!("@carol:localhost")
}
/// Current user keys query response containing the 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 data = response_from_file(&data);
KeyQueryResponse::try_from_http_response(data)
.expect("Can't parse the `/keys/upload` response")
}
pub fn device_keys_payload_bob_unsigned_device() -> Value {
json!({
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": "XCYNVRMTER",
"keys": {
"curve25519:XCYNVRMTER": "xGKYkFcHGlJ+I1yiefPyZu1EY8i2h1eed5uk3PAW6GA",
"ed25519:XCYNVRMTER": "EsU8MJzTYE+/VJs1K9HkGqb8UXCByPioynGrV28WocU"
},
"signatures": {
"@bob:localhost": {
"ed25519:XCYNVRMTER": "yZ7cpaoA+0rRx+bmklsP1iAd0eGPH6gsdywC11VE98/mrcbeFuxjQVn39Ds7h+vmciu5GRzwWgDgv+6go6FHAQ",
// Remove the cross-signature
// "ed25519:e8JFSrW8LW3UK6SSXh2ZESUzptFbapr28/+WqndD+Xk": "xYnGmU9FEdoavB5P743gx3xbEy29tlfRX5lT3JO0dWhHdsP+muqBXUYMBl1RRFeZtIE0GYc9ORb6Yf88EdeoCw"
}
},
"user_id": "@bob:localhost",
"unsigned": {}
})
}
pub fn bob_keys_query_response_signed() -> KeyQueryResponse {
let data = json!({
"device_keys": {
"@bob:localhost": {
"RLZGZIHKMP": {
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": "RLZGZIHKMP",
"keys": {
"curve25519:RLZGZIHKMP": "Zd8uO9Rr1PtqNno3//ybeUZ3JuqFtm17TQTWW0f47AU",
"ed25519:RLZGZIHKMP": "kH+Zn2m7LPES/XLOyVvnf8t4Byfj3mAbngHptHZFzk0"
},
"signatures": {
"@bob:localhost": {
"ed25519:RLZGZIHKMP": "w4MOkDiD+4XatQrRzGrcaqwVmiZrAjxmaIA8aSuzQveD2SJ2eVZq3OSpqx6QRUbG/gkkZxGmY13PkS/iAOv0AA",
"ed25519:e8JFSrW8LW3UK6SSXh2ZESUzptFbapr28/+WqndD+Xk": "ki+cV0EVe5cYXnzqU078qy1qu2rnaxaBQU+KwyvPpEUotNTXjWKUOJfxast42d5tjI5vsI5aiQ6XkYfjBJ74Bw"
}
},
"user_id": "@bob:localhost",
"unsigned": {}
},
"XCYNVRMTER": Self::device_keys_payload_bob_unsigned_device(),
}
},
"failures": {},
"master_keys": {
"@bob:localhost": {
"keys": {
"ed25519:xZPyb4hxM8zaedDFz5m8HsDpX1fknd/V/69THLhNX9I": "xZPyb4hxM8zaedDFz5m8HsDpX1fknd/V/69THLhNX9I"
},
"signatures": {
"@bob:localhost": {
"ed25519:RLZGZIHKMP": "5bHLrx0HwYsNRtd65s1a1wVGlwgJU8yb8cq/Qbq04o9nVdQuY8+woQVWq9nxk59u6QFZIpFdVjXsuTPkDJLsBA",
"ed25519:xZPyb4hxM8zaedDFz5m8HsDpX1fknd/V/69THLhNX9I": "NA+cLNIPpmECcBIcmAH5l1K4IDXI6Xss1VmU8TZ04AYQSAh/2sv7NixEBO1/Raz0nErzkOl8gpRswHbHv1p7Dw"
},
"@alice:localhost": {
"ed25519:MXob/N/bYI7U2655O1/AI9NOX1245RnE03Nl4Hvf+u0": "n3X6afWYoSywqBpPlaDfQ2BNjl3ez5AzxEVwaB5/KEAzgwsq5B2qBW9N5uZaNWEq5M3JBrh0doj1FgUg4R3yBQ"
}
},
"usage": [
"master"
],
"user_id": "@bob:localhost"
}
},
"self_signing_keys": {
"@bob:localhost": {
"keys": {
"ed25519:e8JFSrW8LW3UK6SSXh2ZESUzptFbapr28/+WqndD+Xk": "e8JFSrW8LW3UK6SSXh2ZESUzptFbapr28/+WqndD+Xk"
},
"signatures": {
"@bob:localhost": {
"ed25519:xZPyb4hxM8zaedDFz5m8HsDpX1fknd/V/69THLhNX9I": "kkGZHLY18jyqXs412VB31u6vxijbaBgVrIMR/LBAFULhTZk6HGH951N6NxMZnYHyH0sFaQhsl4DUqt7XthBHBQ"
}
},
"usage": [
"self_signing"
],
"user_id": "@bob:localhost"
}
},
"user_signing_keys": {}
});
let data = response_from_file(&data);
KeyQueryResponse::try_from_http_response(data)
.expect("Can't parse the `/keys/upload` response")
}
pub fn bob_device_1_id() -> &'static DeviceId {
device_id!("RLZGZIHKMP")
}
pub fn bob_device_2_id() -> &'static DeviceId {
device_id!("XCYNVRMTER")
}
// Bob has a new identity, the two devices are properly self-signed
pub fn bob_keys_query_response_rotated() -> KeyQueryResponse {
let data = json!({
"device_keys": {
"@bob:localhost": {
"RLZGZIHKMP": {
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": "RLZGZIHKMP",
"keys": {
"curve25519:RLZGZIHKMP": "Zd8uO9Rr1PtqNno3//ybeUZ3JuqFtm17TQTWW0f47AU",
"ed25519:RLZGZIHKMP": "kH+Zn2m7LPES/XLOyVvnf8t4Byfj3mAbngHptHZFzk0"
},
"signatures": {
"@bob:localhost": {
"ed25519:RLZGZIHKMP": "w4MOkDiD+4XatQrRzGrcaqwVmiZrAjxmaIA8aSuzQveD2SJ2eVZq3OSpqx6QRUbG/gkkZxGmY13PkS/iAOv0AA",
// "ed25519:At1ai1VUZrCncCI7V7fEAJmBShfpqZ30xRzqcEjTjdc": "rg3b3DovN3VztdcKyOcOlIGQxmm+8VC9+ImuXdgug/kPSi7QcljwOtjnk4LMkHexB3xVzB0ANcyNjbJ2cJuYBg",
"ed25519:e8JFSrW8LW3UK6SSXh2ZESUzptFbapr28/+WqndD+Xk": "ki+cV0EVe5cYXnzqU078qy1qu2rnaxaBQU+KwyvPpEUotNTXjWKUOJfxast42d5tjI5vsI5aiQ6XkYfjBJ74Bw"
}
},
"user_id": "@bob:localhost",
"unsigned": {
"device_display_name": "develop.element.io: Chrome on macOS"
}
},
"XCYNVRMTER": {
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": "XCYNVRMTER",
"keys": {
"curve25519:XCYNVRMTER": "xGKYkFcHGlJ+I1yiefPyZu1EY8i2h1eed5uk3PAW6GA",
"ed25519:XCYNVRMTER": "EsU8MJzTYE+/VJs1K9HkGqb8UXCByPioynGrV28WocU"
},
"signatures": {
"@bob:localhost": {
"ed25519:XCYNVRMTER": "yZ7cpaoA+0rRx+bmklsP1iAd0eGPH6gsdywC11VE98/mrcbeFuxjQVn39Ds7h+vmciu5GRzwWgDgv+6go6FHAQ",
"ed25519:e8JFSrW8LW3UK6SSXh2ZESUzptFbapr28/+WqndD+Xk": "xYnGmU9FEdoavB5P743gx3xbEy29tlfRX5lT3JO0dWhHdsP+muqBXUYMBl1RRFeZtIE0GYc9ORb6Yf88EdeoCw",
"ed25519:NWoyMF4Ox8PEj+8l1e70zuIUg0D+wL9wtcj1KhWL0Bc": "2ieX8z+oW9JhdyIIkTDsQ2o5VWxcO6dOgeyPbRwbAL6Q8J6xujzYSIi568UAlPt+wg+RkNLshneexCPNMgSiDQ"
}
},
"user_id": "@bob:localhost",
"unsigned": {
"device_display_name": "app.element.io: Chrome on mac"
}
}
}
},
"failures": {},
"master_keys": {
"@bob:localhost": {
"keys": {
"ed25519:xaFlsDqlDRRy7Idtt1dW9mdhH/gvvax34q+HxepjNWY": "xaFlsDqlDRRy7Idtt1dW9mdhH/gvvax34q+HxepjNWY"
},
"signatures": {
"@bob:localhost": {
"ed25519:XCYNVRMTER": "K1aPl+GtcNi8yDqn1zvKIJMg3PFLQkwoXJeFJMmct4SA2SiQIl1S2x1bDTC3kQ4/LA7ULiQgKlxkXdQVf2GZDw",
"ed25519:xaFlsDqlDRRy7Idtt1dW9mdhH/gvvax34q+HxepjNWY": "S5vw8moiPudKhmF1qIv3/ehbZ7uohJbcQaLcOV+DDh9iC/YX0UqnaGn1ZYWJpIN7Kxe2ZWCBwzp35DOVZKfxBw"
}
},
"usage": [
"master"
],
"user_id": "@bob:localhost"
}
},
"self_signing_keys": {
"@bob:localhost": {
"keys": {
"ed25519:NWoyMF4Ox8PEj+8l1e70zuIUg0D+wL9wtcj1KhWL0Bc": "NWoyMF4Ox8PEj+8l1e70zuIUg0D+wL9wtcj1KhWL0Bc"
},
"signatures": {
"@bob:localhost": {
"ed25519:xaFlsDqlDRRy7Idtt1dW9mdhH/gvvax34q+HxepjNWY": "rwQIkR7JbZOrwGrmkW9QzFlK+lMjRDHVcGVlYNS/zVeDyvWxD0WFHcmy4p/LSgJDyrVt+th7LH7Bj+Ed/EGvCw"
}
},
"usage": [
"self_signing"
],
"user_id": "@bob:localhost"
}
},
"user_signing_keys": {}
});
let data = response_from_file(&data);
KeyQueryResponse::try_from_http_response(data)
.expect("Can't parse the `/keys/upload` response")
}
pub fn device_1_keys_payload_carol() -> Value {
json!({
// Not self signed
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": "BAZAPVEHGA",
"keys": {
"curve25519:BAZAPVEHGA": "/mCcWJb5mtNGPC7m4iQeW8gVJB4nG8z/z2QQXzzNijw",
"ed25519:BAZAPVEHGA": "MLSoOlk27qcS/2O9Etp6XwgF8j+UT06yy/ypSeE9JRA"
},
"signatures": {
"@carol:localhost": {
"ed25519:BAZAPVEHGA": "y2+Z0ofRRoNMj864SoAcNEXRToYVeiARu39CO0Vj2GcSIxlpR7B8K1wDYV4luP4gOL1t1tPgJPXL1WO//AHHCw",
}
},
"user_id": "@carol:localhost"
})
}
pub fn device_2_keys_payload_carol() -> Value {
// Self-signed device
json!({
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": "JBRBCHOFDZ",
"keys": {
"curve25519:JBRBCHOFDZ": "900HdrlfxlH8yMSmEQ3C32uVyXCuxKs5oPKS/wUgzVQ",
"ed25519:JBRBCHOFDZ": "BOINY06uroLYscHUq0e0FmUo/W0LC4/fsIPkZQe71NY"
},
"signatures": {
"@carol:localhost": {
"ed25519:JBRBCHOFDZ": "MmSJS3yEdeuseiLTDCQwImZBPNFMdhhkAFjRZZrIONoGFR0AMSzgLtx/nSgXP8RwVxpycvb6OAqvSk2toK3PDg",
"ed25519:ZOMWgk5LAogkwDEdZl9Rv7FRGu0nGbeLtMHx6anzhQs": "VtoxmPn/BQVDlpEHPEI2wPUlruUX9m2zV3FChNkRyEEWur4St27WA1He8BwjVRiiT0bdUnVH3xfmucoV9UnbDA"
}
},
"user_id": "@carol:localhost",
})
}
pub fn ssk_payload_carol() -> Value {
json!({
"@carol:localhost": {
"keys": {
"ed25519:ZOMWgk5LAogkwDEdZl9Rv7FRGu0nGbeLtMHx6anzhQs": "ZOMWgk5LAogkwDEdZl9Rv7FRGu0nGbeLtMHx6anzhQs"
},
"signatures": {
"@carol:localhost": {
"ed25519:itnwUCRfBPW08IrmBks9MTp/Qm5AJ2WNca13ptIZF8U": "thjR1/kxHADXqLqxc4Q3OZhAaLq7SPL96LNCGVGN64OYAJ5yG1cpqAXBiBCUaBUTdRTb0ys601RR8djPdTK/BQ"
}
},
"usage": [
"self_signing"
],
"user_id": "@carol:localhost"
}
})
}
// Carol key query response with one signed and one unsigned device.
// Bob has not verified Carol yet
pub fn carol_keys_query_response_unsigned() -> KeyQueryResponse {
let data = json!({
"device_keys": {
"@carol:localhost": {
"BAZAPVEHGA": Self::device_1_keys_payload_carol(),
"JBRBCHOFDZ": Self::device_2_keys_payload_carol()
}
},
"failures": {},
"master_keys": {
"@carol:localhost": {
"keys": {
"ed25519:itnwUCRfBPW08IrmBks9MTp/Qm5AJ2WNca13ptIZF8U": "itnwUCRfBPW08IrmBks9MTp/Qm5AJ2WNca13ptIZF8U"
},
"signatures": {
"@carol:localhost": {
"ed25519:JBRBCHOFDZ": "eRA4jRSszQVuYpMtHTBuWGLEzcdUojyCW4/XKHRIQ2solv7iTC/MWES6I20YrHJa7H82CVoyNxS1Y3AwttBbCg",
"ed25519:itnwUCRfBPW08IrmBks9MTp/Qm5AJ2WNca13ptIZF8U": "e3r5L+JLv6FB8+Tt4BlIbz4wk2qPeMoKL1uR079qZzYMvtKoWGK9p000cZIhA5R1Tl7buQ9ODUfizued8g3TAg"
},
// "@alice:localhost": {
// "ed25519:MXob/N/bYI7U2655O1/AI9NOX1245RnE03Nl4Hvf+u0": "yfRUvkaVg3KizC/HDXcuP4+gtYhxgzr8X916Wt4GRXjj4qhDjsCkf8mYZ7x4lcEXzRkYql5KelabgVzP12qmAA"
// }
},
"usage": [
"master"
],
"user_id": "@carol:localhost"
}
},
"self_signing_keys": Self::ssk_payload_carol(),
"user_signing_keys": {}
});
let data = response_from_file(&data);
KeyQueryResponse::try_from_http_response(data)
.expect("Can't parse the `/keys/upload` response")
}
pub fn carol_keys_query_response_signed() -> KeyQueryResponse {
let data = json!({
"device_keys": {
"@carol:localhost": {
"BAZAPVEHGA": Self::device_1_keys_payload_carol(),
"JBRBCHOFDZ": Self::device_2_keys_payload_carol()
}
},
"failures": {},
"master_keys": {
"@carol:localhost": {
"keys": {
"ed25519:itnwUCRfBPW08IrmBks9MTp/Qm5AJ2WNca13ptIZF8U": "itnwUCRfBPW08IrmBks9MTp/Qm5AJ2WNca13ptIZF8U"
},
"signatures": {
"@carol:localhost": {
"ed25519:JBRBCHOFDZ": "eRA4jRSszQVuYpMtHTBuWGLEzcdUojyCW4/XKHRIQ2solv7iTC/MWES6I20YrHJa7H82CVoyNxS1Y3AwttBbCg",
"ed25519:itnwUCRfBPW08IrmBks9MTp/Qm5AJ2WNca13ptIZF8U": "e3r5L+JLv6FB8+Tt4BlIbz4wk2qPeMoKL1uR079qZzYMvtKoWGK9p000cZIhA5R1Tl7buQ9ODUfizued8g3TAg"
},
"@alice:localhost": {
"ed25519:MXob/N/bYI7U2655O1/AI9NOX1245RnE03Nl4Hvf+u0": "yfRUvkaVg3KizC/HDXcuP4+gtYhxgzr8X916Wt4GRXjj4qhDjsCkf8mYZ7x4lcEXzRkYql5KelabgVzP12qmAA"
}
},
"usage": [
"master"
],
"user_id": "@carol:localhost"
}
},
"self_signing_keys": Self::ssk_payload_carol(),
"user_signing_keys": {}
});
let data = response_from_file(&data);
KeyQueryResponse::try_from_http_response(data)
.expect("Can't parse the `/keys/upload` response")
}
}