Skip to content

Commit 2cd9462

Browse files
authored
Famedly Release v1.138.4_1 (#152)
2 parents b14601b + 08a9e69 commit 2cd9462

8 files changed

Lines changed: 335 additions & 38 deletions

File tree

CHANGES.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
# Synapse 1.138.4 (2025-10-07)
2+
3+
## Bugfixes
4+
5+
- Fix a bug introduced in 1.138.3 where a client could receive an Internal Server Error if they set `device_keys: null` in the request to [`POST /_matrix/client/v3/keys/upload`](https://spec.matrix.org/v1.16/client-server-api/#post_matrixclientv3keysupload). ([\#19023](https://github.com/element-hq/synapse/issues/19023))
6+
7+
8+
9+
10+
# Synapse 1.138.3 (2025-10-07)
11+
12+
## Security Fixes
13+
14+
- Fix [CVE-2025-61672](https://www.cve.org/CVERecord?id=CVE-2025-61672) / [GHSA-fh66-fcv5-jjfr](https://github.com/element-hq/synapse/security/advisories/GHSA-fh66-fcv5-jjfr). Lack of validation for device keys in Synapse before 1.139.1 allows an attacker registered on the victim homeserver to degrade federation functionality, unpredictably breaking outbound federation to other homeservers. ([\#17097](https://github.com/element-hq/synapse/issues/17097))
15+
16+
## Deprecations and Removals
17+
18+
- Drop support for unstable field names from the long-accepted [MSC2732](https://github.com/matrix-org/matrix-spec-proposals/pull/2732) (Olm fallback keys) proposal. This change allows unit tests to pass following the security patch above. ([\#18996](https://github.com/element-hq/synapse/issues/18996))
19+
20+
21+
22+
123
# Synapse 1.138.2 (2025-09-24)
224

325
## Internal Changes

debian/changelog

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
matrix-synapse-py3 (1.138.4) stable; urgency=medium
2+
3+
* New Synapse release 1.138.4.
4+
5+
-- Synapse Packaging team <packages@matrix.org> Tue, 07 Oct 2025 16:28:38 +0100
6+
7+
matrix-synapse-py3 (1.138.3) stable; urgency=medium
8+
9+
* New Synapse release 1.138.3.
10+
11+
-- Synapse Packaging team <packages@matrix.org> Tue, 07 Oct 2025 12:54:18 +0100
12+
113
matrix-synapse-py3 (1.138.2) stable; urgency=medium
214

315
* New Synapse release 1.138.2.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ module-name = "synapse.synapse_rust"
101101

102102
[tool.poetry]
103103
name = "matrix-synapse"
104-
version = "1.138.2"
104+
version = "1.138.4"
105105
description = "Homeserver for the Matrix decentralised comms protocol"
106106
authors = ["Matrix.org Team and Contributors <packages@matrix.org>"]
107107
license = "AGPL-3.0-or-later"

synapse/handlers/e2e_keys.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@
5757

5858
logger = logging.getLogger(__name__)
5959

60-
6160
ONE_TIME_KEY_UPLOAD = "one_time_key_upload_lock"
6261

6362

@@ -848,14 +847,22 @@ async def upload_keys_for_user(
848847
"""
849848
time_now = self.clock.time_msec()
850849

851-
# TODO: Validate the JSON to make sure it has the right keys.
852850
device_keys = keys.get("device_keys", None)
853851
if device_keys:
852+
log_kv(
853+
{
854+
"message": "Updating device_keys for user.",
855+
"user_id": user_id,
856+
"device_id": device_id,
857+
}
858+
)
854859
await self.upload_device_keys_for_user(
855860
user_id=user_id,
856861
device_id=device_id,
857862
keys={"device_keys": device_keys},
858863
)
864+
else:
865+
log_kv({"message": "Did not update device_keys", "reason": "not a dict"})
859866

860867
one_time_keys = keys.get("one_time_keys", None)
861868
if one_time_keys:
@@ -873,10 +880,9 @@ async def upload_keys_for_user(
873880
log_kv(
874881
{"message": "Did not update one_time_keys", "reason": "no keys given"}
875882
)
876-
fallback_keys = keys.get("fallback_keys") or keys.get(
877-
"org.matrix.msc2732.fallback_keys"
878-
)
879-
if fallback_keys and isinstance(fallback_keys, dict):
883+
884+
fallback_keys = keys.get("fallback_keys")
885+
if fallback_keys:
880886
log_kv(
881887
{
882888
"message": "Updating fallback_keys for device.",
@@ -885,8 +891,6 @@ async def upload_keys_for_user(
885891
}
886892
)
887893
await self.store.set_e2e_fallback_keys(user_id, device_id, fallback_keys)
888-
elif fallback_keys:
889-
log_kv({"message": "Did not update fallback_keys", "reason": "not a dict"})
890894
else:
891895
log_kv(
892896
{"message": "Did not update fallback_keys", "reason": "no keys given"}

synapse/rest/client/keys.py

Lines changed: 147 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,19 @@
2323
import logging
2424
import re
2525
from collections import Counter
26-
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple
26+
from http import HTTPStatus
27+
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Tuple, Union
2728

29+
from typing_extensions import Self
30+
31+
from synapse._pydantic_compat import (
32+
StrictBool,
33+
StrictStr,
34+
validator,
35+
)
2836
from synapse.api.auth.mas import MasDelegatedAuth
2937
from synapse.api.errors import (
38+
Codes,
3039
InteractiveAuthIncompleteError,
3140
InvalidAPICallError,
3241
SynapseError,
@@ -37,11 +46,13 @@
3746
parse_integer,
3847
parse_json_object_from_request,
3948
parse_string,
49+
validate_json_object,
4050
)
4151
from synapse.http.site import SynapseRequest
4252
from synapse.logging.opentracing import log_kv, set_tag
4353
from synapse.rest.client._base import client_patterns, interactive_auth_handler
4454
from synapse.types import JsonDict, StreamToken
55+
from synapse.types.rest import RequestBodyModel
4556
from synapse.util.cancellation import cancellable
4657

4758
if TYPE_CHECKING:
@@ -59,7 +70,6 @@ class KeyUploadServlet(RestServlet):
5970
"device_keys": {
6071
"user_id": "<user_id>",
6172
"device_id": "<device_id>",
62-
"valid_until_ts": <millisecond_timestamp>,
6373
"algorithms": [
6474
"m.olm.curve25519-aes-sha2",
6575
]
@@ -111,12 +121,123 @@ def __init__(self, hs: "HomeServer"):
111121
self._clock = hs.get_clock()
112122
self._store = hs.get_datastores().main
113123

124+
class KeyUploadRequestBody(RequestBodyModel):
125+
"""
126+
The body of a `POST /_matrix/client/v3/keys/upload` request.
127+
128+
Based on https://spec.matrix.org/v1.16/client-server-api/#post_matrixclientv3keysupload.
129+
"""
130+
131+
class DeviceKeys(RequestBodyModel):
132+
algorithms: List[StrictStr]
133+
"""The encryption algorithms supported by this device."""
134+
135+
device_id: StrictStr
136+
"""The ID of the device these keys belong to. Must match the device ID used when logging in."""
137+
138+
keys: Mapping[StrictStr, StrictStr]
139+
"""
140+
Public identity keys. The names of the properties should be in the
141+
format `<algorithm>:<device_id>`. The keys themselves should be encoded as
142+
specified by the key algorithm.
143+
"""
144+
145+
signatures: Mapping[StrictStr, Mapping[StrictStr, StrictStr]]
146+
"""Signatures for the device key object. A map from user ID, to a map from "<algorithm>:<device_id>" to the signature."""
147+
148+
user_id: StrictStr
149+
"""The ID of the user the device belongs to. Must match the user ID used when logging in."""
150+
151+
class KeyObject(RequestBodyModel):
152+
key: StrictStr
153+
"""The key, encoded using unpadded base64."""
154+
155+
fallback: Optional[StrictBool] = False
156+
"""Whether this is a fallback key. Only used when handling fallback keys."""
157+
158+
signatures: Mapping[StrictStr, Mapping[StrictStr, StrictStr]]
159+
"""Signature for the device. Mapped from user ID to another map of key signing identifier to the signature itself.
160+
161+
See the following for more detail: https://spec.matrix.org/v1.16/appendices/#signing-details
162+
"""
163+
164+
device_keys: Optional[DeviceKeys] = None
165+
"""Identity keys for the device. May be absent if no new identity keys are required."""
166+
167+
fallback_keys: Optional[Mapping[StrictStr, Union[StrictStr, KeyObject]]]
168+
"""
169+
The public key which should be used if the device's one-time keys are
170+
exhausted. The fallback key is not deleted once used, but should be
171+
replaced when additional one-time keys are being uploaded. The server
172+
will notify the client of the fallback key being used through `/sync`.
173+
174+
There can only be at most one key per algorithm uploaded, and the server
175+
will only persist one key per algorithm.
176+
177+
When uploading a signed key, an additional fallback: true key should be
178+
included to denote that the key is a fallback key.
179+
180+
May be absent if a new fallback key is not required.
181+
"""
182+
183+
@validator("fallback_keys", pre=True)
184+
def validate_fallback_keys(cls: Self, v: Any) -> Any:
185+
if v is None:
186+
return v
187+
if not isinstance(v, dict):
188+
raise TypeError("fallback_keys must be a mapping")
189+
190+
for k in v.keys():
191+
if not len(k.split(":")) == 2:
192+
raise SynapseError(
193+
code=HTTPStatus.BAD_REQUEST,
194+
errcode=Codes.BAD_JSON,
195+
msg=f"Invalid fallback_keys key {k!r}. "
196+
'Expected "<algorithm>:<device_id>".',
197+
)
198+
return v
199+
200+
one_time_keys: Optional[Mapping[StrictStr, Union[StrictStr, KeyObject]]] = None
201+
"""
202+
One-time public keys for "pre-key" messages. The names of the properties
203+
should be in the format `<algorithm>:<key_id>`.
204+
205+
The format of the key is determined by the key algorithm, see:
206+
https://spec.matrix.org/v1.16/client-server-api/#key-algorithms.
207+
"""
208+
209+
@validator("one_time_keys", pre=True)
210+
def validate_one_time_keys(cls: Self, v: Any) -> Any:
211+
if v is None:
212+
return v
213+
if not isinstance(v, dict):
214+
raise TypeError("one_time_keys must be a mapping")
215+
216+
for k, _ in v.items():
217+
if not len(k.split(":")) == 2:
218+
raise SynapseError(
219+
code=HTTPStatus.BAD_REQUEST,
220+
errcode=Codes.BAD_JSON,
221+
msg=f"Invalid one_time_keys key {k!r}. "
222+
'Expected "<algorithm>:<device_id>".',
223+
)
224+
return v
225+
114226
async def on_POST(
115227
self, request: SynapseRequest, device_id: Optional[str]
116228
) -> Tuple[int, JsonDict]:
117229
requester = await self.auth.get_user_by_req(request, allow_guest=True)
118230
user_id = requester.user.to_string()
231+
232+
# Parse the request body. Validate separately, as the handler expects a
233+
# plain dict, rather than any parsed object.
234+
#
235+
# Note: It would be nice to work with a parsed object, but the handler
236+
# needs to encode portions of the request body as canonical JSON before
237+
# storing the result in the DB. There's little point in converted to a
238+
# parsed object and then back to a dict.
119239
body = parse_json_object_from_request(request)
240+
validate_json_object(body, self.KeyUploadRequestBody)
120241

121242
if device_id is not None:
122243
# Providing the device_id should only be done for setting keys
@@ -149,8 +270,31 @@ async def on_POST(
149270
400, "To upload keys, you must pass device_id when authenticating"
150271
)
151272

273+
if "device_keys" in body and isinstance(body["device_keys"], dict):
274+
# Validate the provided `user_id` and `device_id` fields in
275+
# `device_keys` match that of the requesting user. We can't do
276+
# this directly in the pydantic model as we don't have access
277+
# to the requester yet.
278+
#
279+
# TODO: We could use ValidationInfo when we switch to Pydantic v2.
280+
# https://docs.pydantic.dev/latest/concepts/validators/#validation-info
281+
if body["device_keys"].get("user_id") != user_id:
282+
raise SynapseError(
283+
code=HTTPStatus.BAD_REQUEST,
284+
errcode=Codes.BAD_JSON,
285+
msg="Provided `user_id` in `device_keys` does not match that of the authenticated user",
286+
)
287+
if body["device_keys"].get("device_id") != device_id:
288+
raise SynapseError(
289+
code=HTTPStatus.BAD_REQUEST,
290+
errcode=Codes.BAD_JSON,
291+
msg="Provided `device_id` in `device_keys` does not match that of the authenticated user device",
292+
)
293+
152294
result = await self.e2e_keys_handler.upload_keys_for_user(
153-
user_id=user_id, device_id=device_id, keys=body
295+
user_id=user_id,
296+
device_id=device_id,
297+
keys=body,
154298
)
155299

156300
return 200, result

synapse/rest/client/sync.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -363,9 +363,6 @@ async def encode_response(
363363

364364
# https://github.com/matrix-org/matrix-doc/blob/54255851f642f84a4f1aaf7bc063eebe3d76752b/proposals/2732-olm-fallback-keys.md
365365
# states that this field should always be included, as long as the server supports the feature.
366-
response["org.matrix.msc2732.device_unused_fallback_key_types"] = (
367-
sync_result.device_unused_fallback_key_types
368-
)
369366
response["device_unused_fallback_key_types"] = (
370367
sync_result.device_unused_fallback_key_types
371368
)

tests/handlers/test_e2e_keys.py

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,6 @@ def test_fallback_key(self) -> None:
410410
device_id = "xyz"
411411
fallback_key = {"alg1:k1": "fallback_key1"}
412412
fallback_key2 = {"alg1:k2": "fallback_key2"}
413-
fallback_key3 = {"alg1:k2": "fallback_key3"}
414413
otk = {"alg1:k2": "key2"}
415414

416415
# we shouldn't have any unused fallback keys yet
@@ -531,28 +530,6 @@ def test_fallback_key(self) -> None:
531530
{"failures": {}, "one_time_keys": {local_user: {device_id: fallback_key2}}},
532531
)
533532

534-
# using the unstable prefix should also set the fallback key
535-
self.get_success(
536-
self.handler.upload_keys_for_user(
537-
local_user,
538-
device_id,
539-
{"org.matrix.msc2732.fallback_keys": fallback_key3},
540-
)
541-
)
542-
543-
claim_res = self.get_success(
544-
self.handler.claim_one_time_keys(
545-
{local_user: {device_id: {"alg1": 1}}},
546-
self.requester,
547-
timeout=None,
548-
always_include_fallback_keys=False,
549-
)
550-
)
551-
self.assertEqual(
552-
claim_res,
553-
{"failures": {}, "one_time_keys": {local_user: {device_id: fallback_key3}}},
554-
)
555-
556533
def test_fallback_key_bulk(self) -> None:
557534
"""Like test_fallback_key, but claims multiple keys in one handler call."""
558535
alice = f"@alice:{self.hs.hostname}"

0 commit comments

Comments
 (0)