Compare commits

..

11 Commits

Author SHA1 Message Date
Damir Jelić 76d1f8bd18 chore: Fix a PR link in the changelog file 2025-06-10 12:37:32 +02:00
Damir Jelić 550f4c5fde Update crates/matrix-sdk-crypto/CHANGELOG.md
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Signed-off-by: Damir Jelić <poljar@termina.org.uk>
2025-06-10 12:37:32 +02:00
Damir Jelić b3f07f4587 chore: Release matrix-sdk version 0.11.1 2025-06-10 12:37:32 +02:00
Damir Jelić 56980745b4 chore: Add a changelog entry for GHSA-x958-rvg6-956w 2025-06-10 12:37:32 +02:00
Richard van der Hoff 13c1d20482 fix(crypto): Check the sender of an event matches owner of session
Having decrypted an event with a given megolm session, we need to check that
the owner of that session actually matches the sender of an event, otherwise
there is a danger of the sender being spoofed to make it look like it was sent
by another user.

Security-Impact: High
CVE: CVE-2025-48937
GitHub-Advisory: GHSA-x958-rvg6-956w
2025-06-10 12:07:10 +02:00
Richard van der Hoff 7f3e144cb3 refactor (crypto): clarify some comments 2025-06-10 12:07:10 +02:00
Richard van der Hoff fe8bd2fdf3 refactor(crypto): Break get_or_update_verification_state in two
Split this into `get_room_event_verification_state` and
`get_or_update_sender_data`, which I think is a bit clearer.
2025-06-10 12:07:10 +02:00
Damir Jelić aa67148247 refactor(xtask): Use a helper to append options for the release tasks 2025-06-10 09:47:42 +02:00
Damir Jelić 769fcdb1fb feat: Support releasing a specific package 2025-06-10 09:47:42 +02:00
Damir Jelić 6e628781c0 release: Add a changelog entry for the tracing-attributes issue 2025-06-09 20:29:09 +02:00
VerdeQuar a75a2b4113 fix(crypto): Remove wildcard enum variant import
Signed-off-by: VerdeQuar <verdequar@gmail.com>
2025-06-09 20:29:09 +02:00
10 changed files with 232 additions and 38 deletions
Generated
+1 -1
View File
@@ -2993,7 +2993,7 @@ dependencies = [
[[package]]
name = "matrix-sdk-crypto"
version = "0.11.0"
version = "0.11.1"
dependencies = [
"aes",
"anyhow",
+1 -1
View File
@@ -104,7 +104,7 @@ zeroize = "1.8.1"
matrix-sdk = { path = "crates/matrix-sdk", version = "0.11.0", default-features = false }
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.11.0" }
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.11.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.11.0" }
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.11.1" }
matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.11.0", default-features = false }
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.11.0" }
+13
View File
@@ -6,6 +6,19 @@ All notable changes to this project will be documented in this file.
## [Unreleased] - ReleaseDate
## [0.11.1] - 2025-06-10
### Security Fixes
- Check the sender of an event matches owner of session, preventing sender
spoofing by homeserver owners.
[13c1d20](https://github.com/matrix-org/matrix-rust-sdk/commit/13c1d2048286bbabf5e7bc6b015aafee98f04d55) (High, [GHSA-x958-rvg6-956w](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-x958-rvg6-956w)).
### Bug Fixes
- Remove a wildcard enum variant import which breaks compilation if used with
`tracing-attributes` version `0.1.29`. This is a workaround for a bug in
`tracing-attributes`.
([#5190](https://github.com/matrix-org/matrix-rust-sdk/issues/5190)) ([#5191](https://github.com/matrix-org/matrix-rust-sdk/issues/5191)) ([#5193](https://github.com/matrix-org/matrix-rust-sdk/issues/5193))
## [0.11.0] - 2025-04-11
### Features
+1 -1
View File
@@ -9,7 +9,7 @@ name = "matrix-sdk-crypto"
readme = "README.md"
repository = "https://github.com/matrix-org/matrix-rust-sdk"
rust-version = { workspace = true }
version = "0.11.0"
version = "0.11.1"
[package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"]
@@ -32,7 +32,10 @@ use tracing::{debug, enabled, info, instrument, trace, warn, Level};
use crate::{
error::OlmResult,
identities::{DeviceData, OtherUserIdentityData, OwnUserIdentityData, UserIdentityData},
olm::{InboundGroupSession, PrivateCrossSigningIdentity, SenderDataFinder, SenderDataType},
olm::{
sender_data_finder::SessionDeviceCheckError, InboundGroupSession,
PrivateCrossSigningIdentity, SenderDataFinder, SenderDataType,
},
store::{
caches::SequenceNumber, Changes, DeviceChanges, IdentityChanges, KeyQueryManager,
Result as StoreResult, Store, StoreCache, StoreCacheGuard, UserKeyQueryResult,
@@ -1124,8 +1127,6 @@ impl IdentityManager {
session: &mut InboundGroupSession,
device: &DeviceData,
) -> Result<(), CryptoStoreError> {
use crate::olm::sender_data_finder::SessionDeviceCheckError::*;
match SenderDataFinder::find_using_device_data(&self.store, device.clone(), session).await {
Ok(sender_data) => {
debug!(
@@ -1134,10 +1135,10 @@ impl IdentityManager {
);
session.sender_data = sender_data;
}
Err(CryptoStoreError(e)) => {
Err(SessionDeviceCheckError::CryptoStoreError(e)) => {
return Err(e);
}
Err(MismatchedIdentityKeys(e)) => {
Err(SessionDeviceCheckError::MismatchedIdentityKeys(e)) => {
warn!(
?session,
?device,
+80 -10
View File
@@ -1521,20 +1521,72 @@ impl OlmMachine {
self.inner.key_request_machine.request_key(room_id, &event).await
}
/// Find whether the supplied session is verified, and provide
/// explanation of what is missing/wrong if not.
/// Find whether an event decrypted via the supplied session is verified,
/// and provide explanation of what is missing/wrong if not.
///
/// Stores the updated [`SenderData`] for the session in the store
/// if we find an updated value for it.
///
/// # Arguments
///
/// * `session` - The inbound Megolm session that was used to decrypt the
/// event.
/// * `sender` - The `sender` of that event (as claimed by the envelope of
/// the event).
async fn get_room_event_verification_state(
&self,
session: &InboundGroupSession,
sender: &UserId,
) -> MegolmResult<(VerificationState, Option<OwnedDeviceId>)> {
let sender_data = self.get_or_update_sender_data(session, sender).await?;
// If the user ID in the sender data doesn't match that in the event envelope,
// this event is not from who it appears to be from.
//
// If `sender_data.user_id()` returns `None`, that means we don't have any
// information about the owner of the session (i.e. we have
// `SenderData::UnknownDevice`); in that case we fall through to the
// logic in `sender_data_to_verification_state` which will pick an appropriate
// `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,
)
}
Some(_) | None => {
sender_data_to_verification_state(sender_data, session.has_been_imported())
}
};
Ok((verification_state, device_id))
}
/// Get an up-to-date [`SenderData`] for the given session, suitable for
/// determining if messages decrypted using that session are verified.
///
/// Checks both the stored verification state of the session and a
/// recalculated verification state based on our current knowledge, and
/// returns the more trusted of the two.
///
/// Store the updated [`SenderData`] for this session in the store
/// Stores the updated [`SenderData`] for the session in the store
/// if we find an updated value for it.
async fn get_or_update_verification_state(
///
/// # Arguments
///
/// * `session` - The Megolm session that was used to decrypt the event.
/// * `sender` - The claimed sender of that event.
async fn get_or_update_sender_data(
&self,
session: &InboundGroupSession,
sender: &UserId,
) -> MegolmResult<(VerificationState, Option<OwnedDeviceId>)> {
) -> MegolmResult<SenderData> {
/// Whether we should recalculate the Megolm sender's data, given the
/// current sender data. We only want to recalculate if it might
/// increase trust and allow us to decrypt messages that we
@@ -1555,7 +1607,24 @@ impl OlmMachine {
}
let sender_data = if should_recalculate_sender_data(&session.sender_data) {
// The session is not sure of the sender yet. Calculate it.
// The session is not sure of the sender yet. Try to find a matching device
// belonging to the claimed sender of the recently-received event.
//
// It's worth noting that this could in theory result in unintuitive changes,
// like a session which initially appears to belong to Alice turning into a
// session which belongs to Bob [1]. This could mean that a session initially
// successfully decrypts events from Alice, but then stops decrypting those same
// events once we get an update.
//
// That's ok though: if we get good evidence that the session belongs to Bob,
// it's correct to update the session even if we previously had weak
// evidence it belonged to Alice.
//
// [1] For example: maybe Alice and Bob both publish devices with the *same*
// keys (presumably because they are colluding). Initially we think
// the session belongs to Alice, but then we do a device lookup for
// Bob, we find a matching device with a cross-signature, so prefer
// that.
let calculated_sender_data = SenderDataFinder::find_using_curve_key(
self.store(),
session.sender_key(),
@@ -1581,7 +1650,7 @@ impl OlmMachine {
session.sender_data.clone()
};
Ok(sender_data_to_verification_state(sender_data, session.has_been_imported()))
Ok(sender_data)
}
/// Request missing local secrets from our devices (cross signing private
@@ -1654,7 +1723,7 @@ impl OlmMachine {
sender: &UserId,
) -> MegolmResult<EncryptionInfo> {
let (verification_state, device_id) =
self.get_or_update_verification_state(session, sender).await?;
self.get_room_event_verification_state(session, sender).await?;
let sender = sender.to_owned();
@@ -2074,7 +2143,7 @@ impl OlmMachine {
self.get_session_encryption_info(room_id, content.session_id(), &event.sender).await
}
/// Get encryption info for a megolm session.
/// Get encryption info for an event decrypted with a megolm session.
///
/// This recalculates the [`EncryptionInfo`] data that is returned by
/// [`OlmMachine::decrypt_room_event`], based on the current
@@ -2086,7 +2155,8 @@ impl OlmMachine {
///
/// * `room_id` - The ID of the room where the session is being used.
/// * `session_id` - The ID of the session to get information for.
/// * `sender` - The user ID of the sender who created this session.
/// * `sender` - The (claimed) sender of the event where the session was
/// used.
pub async fn get_session_encryption_info(
&self,
room_id: &RoomId,
@@ -311,6 +311,69 @@ pub async fn mark_alice_identity_as_verified_test_helper(alice: &OlmMachine, bob
.is_verified());
}
/// 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() {
let (alice, bob) = get_machine_pair_with_setup_sessions_test_helper(
tests::alice_id(),
tests::user_id(),
false,
)
.await;
let room_id = room_id!("!test:example.org");
let decryption_settings =
DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
// Alice sends a message to Bob.
let (event, _) = encrypt_message(&alice, room_id, &bob, "Secret message").await;
bob.decrypt_room_event(&event, room_id, &decryption_settings)
.await
.expect("Bob could not decrypt 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::UnsignedDevice)
);
// Alice now sends a second message to Bob, using the same room key, but the HS
// admin rewrites the 'sender' to Charlie.
let encrypted_content = alice
.encrypt_room_event(
room_id,
AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::text_plain(
"spoofed message",
)),
)
.await
.unwrap();
let event = json!({
"event_id": "$xxxxy:example.org",
"origin_server_ts": MilliSecondsSinceUnixEpoch::now(),
"sender": "@charlie:example.org", // Note! spoofed sender
"type": "m.room.encrypted",
"content": encrypted_content,
});
let event = json_convert(&event).unwrap();
bob.decrypt_room_event(&event, room_id, &decryption_settings)
.await
.expect("Bob could not decrypt spoofed event");
// 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))
);
}
#[async_test]
async fn test_verification_states_multiple_device() {
let (bob, _) = get_prepared_machine_test_helper(tests::user_id(), false).await;
@@ -358,7 +421,7 @@ async fn test_verification_states_multiple_device() {
.unwrap();
let (state, _) = bob
.get_or_update_verification_state(&web_unverified_inbound_session, other_user_id)
.get_room_event_verification_state(&web_unverified_inbound_session, other_user_id)
.await
.unwrap();
assert_eq!(VerificationState::Unverified(VerificationLevel::UnsignedDevice), state);
@@ -376,7 +439,7 @@ async fn test_verification_states_multiple_device() {
.unwrap();
let (state, _) = bob
.get_or_update_verification_state(&web_signed_inbound_session, other_user_id)
.get_room_event_verification_state(&web_signed_inbound_session, other_user_id)
.await
.unwrap();
@@ -252,6 +252,26 @@ impl SenderData {
Self::SenderVerified { .. } => SenderDataType::SenderVerified,
}
}
/// Return our best guess of the owner of the associated megolm session.
///
/// For `SenderData::UnknownDevice`, we don't record any information about
/// the owner of the sender, so returns `None`.
pub(crate) fn user_id(&self) -> Option<OwnedUserId> {
match &self {
SenderData::UnknownDevice { .. } => None,
SenderData::DeviceInfo { device_keys, .. } => Some(device_keys.user_id.clone()),
SenderData::VerificationViolation(known_sender_data) => {
Some(known_sender_data.user_id.clone())
}
SenderData::SenderUnverified(known_sender_data) => {
Some(known_sender_data.user_id.clone())
}
SenderData::SenderVerified(known_sender_data) => {
Some(known_sender_data.user_id.clone())
}
}
}
}
/// Used when deserialising and the sender_data property is missing.
+7 -1
View File
@@ -1375,12 +1375,18 @@ impl Room {
Ok(event)
}
/// Fetches the [`EncryptionInfo`] for the supplied session_id.
/// Fetches the [`EncryptionInfo`] for an event decrypted with the supplied
/// session_id.
///
/// This may be used when we receive an update for a session, and we want to
/// reflect the changes in messages we have received that were encrypted
/// with that session, e.g. to remove a warning shield because a device is
/// now verified.
///
/// # Arguments
/// * `session_id` - The ID of the Megolm session to get information for.
/// * `sender` - The (claimed) sender of the event where the session was
/// used.
#[cfg(feature = "e2e-encryption")]
pub async fn get_encryption_info(
&self,
+38 -17
View File
@@ -1,5 +1,5 @@
use clap::{Args, Subcommand, ValueEnum};
use xshell::cmd;
use xshell::{cmd, Cmd};
use crate::{sh, Result};
@@ -13,15 +13,19 @@ pub struct ReleaseArgs {
enum ReleaseCommand {
/// Prepare the release of the matrix-sdk workspace.
///
/// This command will update the `README.md`, prepend the `CHANGELOG.md`
/// file using `git cliff`, and bump the versions in the `Cargo.toml`
/// files.
/// This command will update the `README.md`, update the `CHANGELOG.md` file
/// using, and bump the versions in the `Cargo.toml` files.
Prepare {
/// What type of version bump we should perform.
version: ReleaseVersion,
/// Actually prepare a release. Dry-run mode is the default.
#[clap(long)]
execute: bool,
/// The crate or package that should be released. Use this if you'd like
/// to release only one specific crate. The default is to
/// release all crates.
#[clap(long)]
package: Option<String>,
},
/// Publish the release.
///
@@ -31,6 +35,11 @@ enum ReleaseCommand {
/// Actually publish a release. Dry-run mode is the default
#[clap(long)]
execute: bool,
/// The crate or package that should be released. Use this if you'd like
/// to release only one specific crate. The default is to
/// release all crates.
#[clap(long)]
package: Option<String>,
},
/// Get a list of interesting changes that happened in the last week.
WeeklyReport,
@@ -63,8 +72,10 @@ impl ReleaseArgs {
check_prerequisites();
match self.cmd {
ReleaseCommand::Prepare { version, execute } => prepare(version, execute),
ReleaseCommand::Publish { execute } => publish(execute),
ReleaseCommand::Prepare { version, execute, package } => {
prepare(version, execute, package)
}
ReleaseCommand::Publish { execute, package } => publish(execute, package),
ReleaseCommand::WeeklyReport => weekly_report(),
}
}
@@ -88,11 +99,21 @@ fn check_prerequisites() {
}
}
fn prepare(version: ReleaseVersion, execute: bool) -> Result<()> {
let sh = sh();
let cmd = cmd!(sh, "cargo release --workspace --no-publish --no-tag --no-push");
fn append_options<'a>(command: Cmd<'a>, execute: &bool, package: &Option<String>) -> Cmd<'a> {
let command = if *execute { command.arg("--execute") } else { command };
if let Some(package) = package.as_deref() {
command.args(["--package", package])
} else {
command.arg("--workspace")
}
}
fn prepare(version: ReleaseVersion, execute: bool, package: Option<String>) -> Result<()> {
let sh = sh();
let cmd = cmd!(sh, "cargo release --no-publish --no-tag --no-push");
let cmd = append_options(cmd, &execute, &package);
let cmd = if execute { cmd.arg("--execute") } else { cmd };
let cmd = cmd.arg(version.as_str());
cmd.run()?;
@@ -108,19 +129,19 @@ fn prepare(version: ReleaseVersion, execute: bool) -> Result<()> {
Ok(())
}
fn publish(execute: bool) -> Result<()> {
fn publish(execute: bool, package: Option<String>) -> Result<()> {
let sh = sh();
let cmd = cmd!(sh, "cargo release tag --workspace");
let cmd = if execute { cmd.arg("--execute") } else { cmd };
let cmd = cmd!(sh, "cargo release tag");
let cmd = append_options(cmd, &execute, &package);
cmd.run()?;
let cmd = cmd!(sh, "cargo release publish --workspace");
let cmd = if execute { cmd.arg("--execute") } else { cmd };
let cmd = cmd!(sh, "cargo release publish");
let cmd = append_options(cmd, &execute, &package);
cmd.run()?;
let cmd = cmd!(sh, "cargo release push --workspace");
let cmd = if execute { cmd.arg("--execute") } else { cmd };
let cmd = cmd!(sh, "cargo release push");
let cmd = append_options(cmd, &execute, &package);
cmd.run()?;
Ok(())