Merge pull request #5219 from matrix-org/rav/megolm_sender_verification_main

crypto: new `VerificationLevel::MismatchedSender`
This commit is contained in:
Richard van der Hoff
2025-06-12 12:44:47 +01:00
committed by GitHub
4 changed files with 74 additions and 28 deletions

View File

@@ -42,6 +42,8 @@ const VERIFICATION_VIOLATION: &str =
"Encrypted by a previously-verified user who is no longer verified.";
const UNSIGNED_DEVICE: &str = "Encrypted by a device not verified by its owner.";
const UNKNOWN_DEVICE: &str = "Encrypted by an unknown or deleted device.";
const MISMATCHED_SENDER: &str =
"The sender of the event does not match the owner of the device that created the Megolm session.";
pub const SENT_IN_CLEAR: &str = "Not encrypted.";
/// Represents the state of verification for a decrypted message sent by a
@@ -117,6 +119,10 @@ impl VerificationState {
message: AUTHENTICITY_NOT_GUARANTEED,
},
},
VerificationLevel::MismatchedSender => ShieldState::Red {
code: ShieldStateCode::MismatchedSender,
message: MISMATCHED_SENDER,
},
},
}
}
@@ -171,6 +177,10 @@ impl VerificationState {
}
}
},
VerificationLevel::MismatchedSender => ShieldState::Red {
code: ShieldStateCode::MismatchedSender,
message: MISMATCHED_SENDER,
},
},
}
}
@@ -198,6 +208,10 @@ pub enum VerificationLevel {
/// deleted) or because the key to decrypt the message was obtained from
/// an insecure source.
None(DeviceLinkProblem),
/// The `sender` field on the event does not match the owner of the device
/// that established the Megolm session.
MismatchedSender,
}
impl fmt::Display for VerificationLevel {
@@ -211,6 +225,7 @@ impl fmt::Display for VerificationLevel {
"The sending device was not signed by the user's identity"
}
VerificationLevel::None(..) => "The sending device is not known",
VerificationLevel::MismatchedSender => MISMATCHED_SENDER,
};
write!(f, "{display}")
}
@@ -271,6 +286,9 @@ pub enum ShieldStateCode {
/// The sender was previously verified but changed their identity.
#[serde(alias = "PreviouslyVerified")]
VerificationViolation,
/// The `sender` field on the event does not match the owner of the device
/// that established the Megolm session.
MismatchedSender,
}
/// The algorithm specific information of a decrypted event.

View File

@@ -6,17 +6,20 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
- [**breaking**] Add a new `VerificationLevel::MismatchedSender` to indicate that the sender of an event appears to have been tampered with.
([#5219](https://github.com/matrix-org/matrix-rust-sdk/pull/5219))
## [0.12.0] - 2025-06-10
### Features
- [**breaking**] The `ProcessedToDeviceEvent::Decrypted` variant now also have an `EncryptionInfo` field.
Format changed from `Decrypted(Raw<AnyToDeviceEvent>)` to `Decrypted { raw: Raw<AnyToDeviceEvent>, encryption_info: EncryptionInfo) }`
([5074](https://github.com/matrix-org/matrix-rust-sdk/pull/5074))
([#5074](https://github.com/matrix-org/matrix-rust-sdk/pull/5074))
- [**breaking**] Move `session_id` from `EncryptionInfo` to `AlgorithmInfo` as it is megolm specific.
Use `EncryptionInfo::session_id()` helper for quick access.
([4981](https://github.com/matrix-org/matrix-rust-sdk/pull/4981))
([#4981](https://github.com/matrix-org/matrix-rust-sdk/pull/4981))
- Send stable identifier `sender_device_keys` for MSC4147 (Including device
keys with Olm-encrypted events).

View File

@@ -1663,14 +1663,7 @@ impl OlmMachine {
// `DeviceLinkProblem` for `VerificationLevel::None`.
let (verification_state, device_id) = match sender_data.user_id() {
Some(i) if i != sender => {
// For backwards compatibility, we treat this the same as "Unknown device".
// TODO: use a dedicated VerificationLevel here.
(
VerificationState::Unverified(VerificationLevel::None(
DeviceLinkProblem::MissingDevice,
)),
None,
)
(VerificationState::Unverified(VerificationLevel::MismatchedSender), None)
}
Some(_) | None => {
@@ -1967,6 +1960,7 @@ impl OlmMachine {
// Case 4
(VerificationLevel::VerificationViolation, _)
| (VerificationLevel::MismatchedSender, _)
| (VerificationLevel::UnsignedDevice, false)
| (VerificationLevel::None(_), false) => false,
}
@@ -1978,6 +1972,7 @@ impl OlmMachine {
VerificationLevel::UnverifiedIdentity => true,
VerificationLevel::VerificationViolation
| VerificationLevel::MismatchedSender
| VerificationLevel::UnsignedDevice
| VerificationLevel::None(_) => false,
},
@@ -2270,6 +2265,7 @@ impl OlmMachine {
///
/// * `event` - The event to get information for.
/// * `room_id` - The ID of the room where the event was sent to.
#[instrument(skip(self, event), fields(event_id, sender, session_id))]
pub async fn get_room_event_encryption_info(
&self,
event: &Raw<EncryptedEvent>,
@@ -2286,6 +2282,11 @@ impl OlmMachine {
}
};
Span::current()
.record("sender", debug(&event.sender))
.record("event_id", debug(&event.event_id))
.record("session_id", content.session_id());
self.get_session_encryption_info(room_id, content.session_id(), &event.sender).await
}

View File

@@ -46,8 +46,8 @@ use crate::{
CrossSigningKey, DeviceKeys, EventEncryptionAlgorithm, MasterPubkey, SelfSigningPubkey,
},
utilities::json_convert,
CryptoStoreError, DecryptionSettings, DeviceData, EncryptionSettings, LocalTrust, OlmMachine,
OtherUserIdentityData, TrustRequirement, UserIdentity,
CryptoStoreError, DecryptionSettings, DeviceData, EncryptionSettings, LocalTrust, MegolmError,
OlmMachine, OtherUserIdentityData, TrustRequirement, UserIdentity,
};
#[async_test]
@@ -311,23 +311,37 @@ pub async fn mark_alice_identity_as_verified_test_helper(alice: &OlmMachine, bob
.is_verified());
}
#[async_test]
async fn test_verification_states_spoofed_sender_untrusted() {
test_verification_states_spoofed_sender(TrustRequirement::Untrusted).await;
}
#[async_test]
async fn test_verification_states_spoofed_sender_cross_signed() {
test_verification_states_spoofed_sender(TrustRequirement::CrossSigned).await;
}
/// Test that the verification state is set correctly when the sender of an
/// event does not match the owner of the device that sent us the session.
///
/// In this test, Bob receives an event from Alice, but the HS admin has
/// rewritten the `sender` of the event to look like another user.
#[async_test]
async fn test_verification_states_spoofed_sender() {
///
/// We run this test a couple of times, with different [`TrustRequirement`]s.
async fn test_verification_states_spoofed_sender(
sender_device_trust_requirement: TrustRequirement,
) {
let (alice, bob) = get_machine_pair_with_setup_sessions_test_helper(
tests::alice_id(),
tests::user_id(),
false,
)
.await;
bob.bootstrap_cross_signing(false).await.unwrap();
set_up_alice_cross_signing(&alice, &bob).await;
let room_id = room_id!("!test:example.org");
let decryption_settings =
DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
let decryption_settings = DecryptionSettings { sender_device_trust_requirement };
// Alice sends a message to Bob.
let (event, _) = encrypt_message(&alice, room_id, &bob, "Secret message").await;
@@ -337,7 +351,7 @@ async fn test_verification_states_spoofed_sender() {
let event_encryption_info = bob.get_room_event_encryption_info(&event, room_id).await.unwrap();
assert_matches!(
&event_encryption_info.verification_state,
VerificationState::Unverified(VerificationLevel::UnsignedDevice)
VerificationState::Unverified(VerificationLevel::UnverifiedIdentity)
);
// Alice now sends a second message to Bob, using the same room key, but the HS
@@ -360,18 +374,28 @@ async fn test_verification_states_spoofed_sender() {
});
let event = json_convert(&event).unwrap();
bob.decrypt_room_event(&event, room_id, &decryption_settings)
.await
.expect("Bob could not decrypt spoofed event");
let decryption_result = bob.decrypt_room_event(&event, room_id, &decryption_settings).await;
// The verification_state of the event should be `MissingDevice` (since it
// manifests as a message from Charlie which does not correspond to one of
// Charlie's devices).
let event_encryption_info = bob.get_room_event_encryption_info(&event, room_id).await.unwrap();
assert_matches!(
&event_encryption_info.verification_state,
VerificationState::Unverified(VerificationLevel::None(DeviceLinkProblem::MissingDevice))
);
if matches!(sender_device_trust_requirement, TrustRequirement::Untrusted) {
// In "Untrusted" mode, the event is decrypted correctly, but the
// verification_state should be `MismatchedSender`.
decryption_result.expect("Bob could not decrypt spoofed event");
let event_encryption_info =
bob.get_room_event_encryption_info(&event, room_id).await.unwrap();
assert_matches!(
&event_encryption_info.verification_state,
VerificationState::Unverified(VerificationLevel::MismatchedSender)
);
} else {
// In "CrossSigned" mode, we refuse to decrypt the event altogether.
let err =
decryption_result.expect_err("Bob was unexpectedly able to decrypt spoofed event");
assert_matches!(
err,
MegolmError::SenderIdentityNotTrusted(VerificationLevel::MismatchedSender)
);
}
}
#[async_test]