Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit ae4acda

Browse files
authored
Implement MSC3984 to proxy /keys/query requests to appservices. (#15321)
If enabled, for users which are exclusively owned by an application service then the appservice will be queried for devices in addition to any information stored in the Synapse database.
1 parent d9f6949 commit ae4acda

File tree

9 files changed

+298
-48
lines changed

9 files changed

+298
-48
lines changed

changelog.d/15314.feature

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Experimental support for passing One Time Key requests to application services ([MSC3983](https://github.com/matrix-org/matrix-spec-proposals/pull/3983)).
1+
Experimental support for passing One Time Key and device key requests to application services ([MSC3983](https://github.com/matrix-org/matrix-spec-proposals/pull/3983) and [MSC3984](https://github.com/matrix-org/matrix-spec-proposals/pull/3984)).

changelog.d/15321.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Experimental support for passing One Time Key and device key requests to application services ([MSC3983](https://github.com/matrix-org/matrix-spec-proposals/pull/3983) and [MSC3984](https://github.com/matrix-org/matrix-spec-proposals/pull/3984)).

synapse/appservice/api.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@
3030
from typing_extensions import TypeGuard
3131

3232
from synapse.api.constants import EventTypes, Membership, ThirdPartyEntityKind
33-
from synapse.api.errors import CodeMessageException
33+
from synapse.api.errors import CodeMessageException, HttpResponseException
3434
from synapse.appservice import (
3535
ApplicationService,
3636
TransactionOneTimeKeysCount,
3737
TransactionUnusedFallbackKeys,
3838
)
3939
from synapse.events import EventBase
4040
from synapse.events.utils import SerializeEventConfig, serialize_event
41-
from synapse.http.client import SimpleHttpClient
41+
from synapse.http.client import SimpleHttpClient, is_unknown_endpoint
4242
from synapse.types import DeviceListUpdates, JsonDict, ThirdPartyInstanceID
4343
from synapse.util.caches.response_cache import ResponseCache
4444

@@ -393,7 +393,11 @@ async def claim_client_keys(
393393
) -> Tuple[Dict[str, Dict[str, Dict[str, JsonDict]]], List[Tuple[str, str, str]]]:
394394
"""Claim one time keys from an application service.
395395
396+
Note that any error (including a timeout) is treated as the application
397+
service having no information.
398+
396399
Args:
400+
service: The application service to query.
397401
query: An iterable of tuples of (user ID, device ID, algorithm).
398402
399403
Returns:
@@ -422,9 +426,9 @@ async def claim_client_keys(
422426
body,
423427
headers={"Authorization": [f"Bearer {service.hs_token}"]},
424428
)
425-
except CodeMessageException as e:
429+
except HttpResponseException as e:
426430
# The appservice doesn't support this endpoint.
427-
if e.code == 404 or e.code == 405:
431+
if is_unknown_endpoint(e):
428432
return {}, query
429433
logger.warning("claim_keys to %s received %s", uri, e.code)
430434
return {}, query
@@ -444,6 +448,48 @@ async def claim_client_keys(
444448

445449
return response, missing
446450

451+
async def query_keys(
452+
self, service: "ApplicationService", query: Dict[str, List[str]]
453+
) -> Dict[str, Dict[str, Dict[str, JsonDict]]]:
454+
"""Query the application service for keys.
455+
456+
Note that any error (including a timeout) is treated as the application
457+
service having no information.
458+
459+
Args:
460+
service: The application service to query.
461+
query: An iterable of tuples of (user ID, device ID, algorithm).
462+
463+
Returns:
464+
A map of device_keys/master_keys/self_signing_keys/user_signing_keys:
465+
466+
device_keys is a map of user ID -> a map device ID -> device info.
467+
"""
468+
if service.url is None:
469+
return {}
470+
471+
# This is required by the configuration.
472+
assert service.hs_token is not None
473+
474+
uri = f"{service.url}/_matrix/app/unstable/org.matrix.msc3984/keys/query"
475+
try:
476+
response = await self.post_json_get_json(
477+
uri,
478+
query,
479+
headers={"Authorization": [f"Bearer {service.hs_token}"]},
480+
)
481+
except HttpResponseException as e:
482+
# The appservice doesn't support this endpoint.
483+
if is_unknown_endpoint(e):
484+
return {}
485+
logger.warning("query_keys to %s received %s", uri, e.code)
486+
return {}
487+
except Exception as ex:
488+
logger.warning("query_keys to %s threw exception %s", uri, ex)
489+
return {}
490+
491+
return response
492+
447493
def _serialize(
448494
self, service: "ApplicationService", events: Iterable[EventBase]
449495
) -> List[JsonDict]:

synapse/config/experimental.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
7979
"msc3983_appservice_otk_claims", False
8080
)
8181

82+
# MSC3984: Proxying key queries to exclusive ASes.
83+
self.msc3984_appservice_key_query: bool = experimental.get(
84+
"msc3984_appservice_key_query", False
85+
)
86+
8287
# MSC3706 (server-side support for partial state in /send_join responses)
8388
# Synapse will always serve partial state responses to requests using the stable
8489
# query parameter `omit_members`. If this flag is set, Synapse will also serve

synapse/federation/federation_client.py

Lines changed: 6 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
event_from_pdu_json,
6262
)
6363
from synapse.federation.transport.client import SendJoinResponse
64+
from synapse.http.client import is_unknown_endpoint
6465
from synapse.http.types import QueryParams
6566
from synapse.logging.opentracing import SynapseTags, log_kv, set_tag, tag_args, trace
6667
from synapse.types import JsonDict, UserID, get_domain_from_id
@@ -759,43 +760,6 @@ async def get_event_auth(
759760

760761
return signed_auth
761762

762-
def _is_unknown_endpoint(
763-
self, e: HttpResponseException, synapse_error: Optional[SynapseError] = None
764-
) -> bool:
765-
"""
766-
Returns true if the response was due to an endpoint being unimplemented.
767-
768-
Args:
769-
e: The error response received from the remote server.
770-
synapse_error: The above error converted to a SynapseError. This is
771-
automatically generated if not provided.
772-
773-
"""
774-
if synapse_error is None:
775-
synapse_error = e.to_synapse_error()
776-
# MSC3743 specifies that servers should return a 404 or 405 with an errcode
777-
# of M_UNRECOGNIZED when they receive a request to an unknown endpoint or
778-
# to an unknown method, respectively.
779-
#
780-
# Older versions of servers don't properly handle this. This needs to be
781-
# rather specific as some endpoints truly do return 404 errors.
782-
return (
783-
# 404 is an unknown endpoint, 405 is a known endpoint, but unknown method.
784-
(e.code == 404 or e.code == 405)
785-
and (
786-
# Older Dendrites returned a text or empty body.
787-
# Older Conduit returned an empty body.
788-
not e.response
789-
or e.response == b"404 page not found"
790-
# The proper response JSON with M_UNRECOGNIZED errcode.
791-
or synapse_error.errcode == Codes.UNRECOGNIZED
792-
)
793-
) or (
794-
# Older Synapses returned a 400 error.
795-
e.code == 400
796-
and synapse_error.errcode == Codes.UNRECOGNIZED
797-
)
798-
799763
async def _try_destination_list(
800764
self,
801765
description: str,
@@ -887,7 +851,7 @@ async def _try_destination_list(
887851
elif 400 <= e.code < 500 and synapse_error.errcode in failover_errcodes:
888852
failover = True
889853

890-
elif failover_on_unknown_endpoint and self._is_unknown_endpoint(
854+
elif failover_on_unknown_endpoint and is_unknown_endpoint(
891855
e, synapse_error
892856
):
893857
failover = True
@@ -1223,7 +1187,7 @@ async def _do_send_join(
12231187
# If an error is received that is due to an unrecognised endpoint,
12241188
# fallback to the v1 endpoint. Otherwise, consider it a legitimate error
12251189
# and raise.
1226-
if not self._is_unknown_endpoint(e):
1190+
if not is_unknown_endpoint(e):
12271191
raise
12281192

12291193
logger.debug("Couldn't send_join with the v2 API, falling back to the v1 API")
@@ -1297,7 +1261,7 @@ async def _do_send_invite(
12971261
# fallback to the v1 endpoint if the room uses old-style event IDs.
12981262
# Otherwise, consider it a legitimate error and raise.
12991263
err = e.to_synapse_error()
1300-
if self._is_unknown_endpoint(e, err):
1264+
if is_unknown_endpoint(e, err):
13011265
if room_version.event_format != EventFormatVersions.ROOM_V1_V2:
13021266
raise SynapseError(
13031267
400,
@@ -1358,7 +1322,7 @@ async def _do_send_leave(self, destination: str, pdu: EventBase) -> JsonDict:
13581322
# If an error is received that is due to an unrecognised endpoint,
13591323
# fallback to the v1 endpoint. Otherwise, consider it a legitimate error
13601324
# and raise.
1361-
if not self._is_unknown_endpoint(e):
1325+
if not is_unknown_endpoint(e):
13621326
raise
13631327

13641328
logger.debug("Couldn't send_leave with the v2 API, falling back to the v1 API")
@@ -1629,7 +1593,7 @@ async def send_request(
16291593
# If an error is received that is due to an unrecognised endpoint,
16301594
# fallback to the unstable endpoint. Otherwise, consider it a
16311595
# legitimate error and raise.
1632-
if not self._is_unknown_endpoint(e):
1596+
if not is_unknown_endpoint(e):
16331597
raise
16341598

16351599
logger.debug(

synapse/handlers/appservice.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
Dict,
1919
Iterable,
2020
List,
21+
Mapping,
2122
Optional,
2223
Tuple,
2324
Union,
@@ -846,6 +847,10 @@ async def claim_e2e_one_time_keys(
846847
]:
847848
"""Claim one time keys from application services.
848849
850+
Users which are exclusively owned by an application service are sent a
851+
key claim request to check if the application service provides keys
852+
directly.
853+
849854
Args:
850855
query: An iterable of tuples of (user ID, device ID, algorithm).
851856
@@ -901,3 +906,59 @@ async def claim_e2e_one_time_keys(
901906
missing.extend(result[1])
902907

903908
return claimed_keys, missing
909+
910+
async def query_keys(
911+
self, query: Mapping[str, Optional[List[str]]]
912+
) -> Dict[str, Dict[str, Dict[str, JsonDict]]]:
913+
"""Query application services for device keys.
914+
915+
Users which are exclusively owned by an application service are queried
916+
for keys to check if the application service provides keys directly.
917+
918+
Args:
919+
query: map from user_id to a list of devices to query
920+
921+
Returns:
922+
A map from user_id -> device_id -> device details
923+
"""
924+
services = self.store.get_app_services()
925+
926+
# Partition the users by appservice.
927+
query_by_appservice: Dict[str, Dict[str, List[str]]] = {}
928+
for user_id, device_ids in query.items():
929+
if not self.store.get_if_app_services_interested_in_user(user_id):
930+
continue
931+
932+
# Find the associated appservice.
933+
for service in services:
934+
if service.is_exclusive_user(user_id):
935+
query_by_appservice.setdefault(service.id, {})[user_id] = (
936+
device_ids or []
937+
)
938+
continue
939+
940+
# Query each service in parallel.
941+
results = await make_deferred_yieldable(
942+
defer.DeferredList(
943+
[
944+
run_in_background(
945+
self.appservice_api.query_keys,
946+
# We know this must be an app service.
947+
self.store.get_app_service_by_id(service_id), # type: ignore[arg-type]
948+
service_query,
949+
)
950+
for service_id, service_query in query_by_appservice.items()
951+
],
952+
consumeErrors=True,
953+
)
954+
)
955+
956+
# Patch together the results -- they are all independent (since they
957+
# require exclusive control over the users). They get returned as a single
958+
# dictionary.
959+
key_queries: Dict[str, Dict[str, Dict[str, JsonDict]]] = {}
960+
for success, result in results:
961+
if success:
962+
key_queries.update(result)
963+
964+
return key_queries

synapse/handlers/e2e_keys.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ def __init__(self, hs: "HomeServer"):
9191
self._query_appservices_for_otks = (
9292
hs.config.experimental.msc3983_appservice_otk_claims
9393
)
94+
self._query_appservices_for_keys = (
95+
hs.config.experimental.msc3984_appservice_key_query
96+
)
9497

9598
@trace
9699
@cancellable
@@ -497,6 +500,19 @@ async def query_local_devices(
497500
local_query, include_displaynames
498501
)
499502

503+
# Check if the application services have any additional results.
504+
if self._query_appservices_for_keys:
505+
# Query the appservices for any keys.
506+
appservice_results = await self._appservice_handler.query_keys(query)
507+
508+
# Merge results, overriding with what the appservice returned.
509+
for user_id, devices in appservice_results.get("device_keys", {}).items():
510+
# Copy the appservice device info over the homeserver device info, but
511+
# don't completely overwrite it.
512+
results.setdefault(user_id, {}).update(devices)
513+
514+
# TODO Handle cross-signing keys.
515+
500516
# Build the result structure
501517
for user_id, device_keys in results.items():
502518
for device_id, device_info in device_keys.items():

synapse/http/client.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,3 +966,41 @@ def getContext(self) -> SSL.Context:
966966

967967
def creatorForNetloc(self, hostname: bytes, port: int) -> IOpenSSLContextFactory:
968968
return self
969+
970+
971+
def is_unknown_endpoint(
972+
e: HttpResponseException, synapse_error: Optional[SynapseError] = None
973+
) -> bool:
974+
"""
975+
Returns true if the response was due to an endpoint being unimplemented.
976+
977+
Args:
978+
e: The error response received from the remote server.
979+
synapse_error: The above error converted to a SynapseError. This is
980+
automatically generated if not provided.
981+
982+
"""
983+
if synapse_error is None:
984+
synapse_error = e.to_synapse_error()
985+
# MSC3743 specifies that servers should return a 404 or 405 with an errcode
986+
# of M_UNRECOGNIZED when they receive a request to an unknown endpoint or
987+
# to an unknown method, respectively.
988+
#
989+
# Older versions of servers don't properly handle this. This needs to be
990+
# rather specific as some endpoints truly do return 404 errors.
991+
return (
992+
# 404 is an unknown endpoint, 405 is a known endpoint, but unknown method.
993+
(e.code == 404 or e.code == 405)
994+
and (
995+
# Older Dendrites returned a text body or empty body.
996+
# Older Conduit returned an empty body.
997+
not e.response
998+
or e.response == b"404 page not found"
999+
# The proper response JSON with M_UNRECOGNIZED errcode.
1000+
or synapse_error.errcode == Codes.UNRECOGNIZED
1001+
)
1002+
) or (
1003+
# Older Synapses returned a 400 error.
1004+
e.code == 400
1005+
and synapse_error.errcode == Codes.UNRECOGNIZED
1006+
)

0 commit comments

Comments
 (0)