mirror of
https://github.com/element-hq/synapse.git
synced 2024-12-14 11:57:44 +00:00
Pass through unsigned
data in /keys/query
We'd like a mechanism by which a client can add "unsigned" data to their device keys, and have it be accessible by other clients involved in E2EE discussions. Most of this actually already works; the bit that doesn't is that *client-side* `/keys/query` strips out any "unsigned" data from the `/keys/upload` body. (Server-side `/keys/query` follows a different codepath and is fine). This commit adds an experimental option which modifies client-side `/keys/query` so that `unsigned` data is preserved.
This commit is contained in:
parent
81b080f7a2
commit
37d7c506d2
4 changed files with 152 additions and 2 deletions
|
@ -448,3 +448,6 @@ class ExperimentalConfig(Config):
|
||||||
|
|
||||||
# MSC4222: Adding `state_after` to sync v2
|
# MSC4222: Adding `state_after` to sync v2
|
||||||
self.msc4222_enabled: bool = experimental.get("msc4222_enabled", False)
|
self.msc4222_enabled: bool = experimental.get("msc4222_enabled", False)
|
||||||
|
|
||||||
|
# MSC4229: Pass through `unsigned` data from `/keys/upload` to `/keys/query`
|
||||||
|
self.msc4229_enabled: bool = experimental.get("msc4229_enabled", False)
|
||||||
|
|
|
@ -542,7 +542,9 @@ class E2eKeysHandler:
|
||||||
result_dict[user_id] = {}
|
result_dict[user_id] = {}
|
||||||
|
|
||||||
results = await self.store.get_e2e_device_keys_for_cs_api(
|
results = await self.store.get_e2e_device_keys_for_cs_api(
|
||||||
local_query, include_displaynames
|
local_query,
|
||||||
|
include_displaynames,
|
||||||
|
include_uploaded_unsigned_data=self.config.experimental.msc4229_enabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if the application services have any additional results.
|
# Check if the application services have any additional results.
|
||||||
|
|
|
@ -220,12 +220,15 @@ class EndToEndKeyWorkerStore(EndToEndKeyBackgroundStore, CacheInvalidationWorker
|
||||||
self,
|
self,
|
||||||
query_list: Collection[Tuple[str, Optional[str]]],
|
query_list: Collection[Tuple[str, Optional[str]]],
|
||||||
include_displaynames: bool = True,
|
include_displaynames: bool = True,
|
||||||
|
include_uploaded_unsigned_data: bool = False,
|
||||||
) -> Dict[str, Dict[str, JsonDict]]:
|
) -> Dict[str, Dict[str, JsonDict]]:
|
||||||
"""Fetch a list of device keys, formatted suitably for the C/S API.
|
"""Fetch a list of device keys, formatted suitably for the C/S API.
|
||||||
Args:
|
Args:
|
||||||
query_list: List of pairs of user_ids and device_ids.
|
query_list: List of pairs of user_ids and device_ids.
|
||||||
include_displaynames: Whether to include the displayname of returned devices
|
include_displaynames: Whether to include the displayname of returned devices
|
||||||
(if one exists).
|
(if one exists).
|
||||||
|
include_uploaded_unsigned_data: Whether to include uploaded `unsigned` data
|
||||||
|
in the response
|
||||||
Returns:
|
Returns:
|
||||||
Dict mapping from user-id to dict mapping from device_id to
|
Dict mapping from user-id to dict mapping from device_id to
|
||||||
key data. The key data will be a dict in the same format as the
|
key data. The key data will be a dict in the same format as the
|
||||||
|
@ -247,7 +250,13 @@ class EndToEndKeyWorkerStore(EndToEndKeyBackgroundStore, CacheInvalidationWorker
|
||||||
if r is None:
|
if r is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# If there was already an `unsigned` dict in the uploaded key, keep it.
|
||||||
|
# Otherwise, create a new one.
|
||||||
|
if not include_uploaded_unsigned_data or not isinstance(
|
||||||
|
r.get("unsigned"), dict
|
||||||
|
):
|
||||||
r["unsigned"] = {}
|
r["unsigned"] = {}
|
||||||
|
|
||||||
if include_displaynames:
|
if include_displaynames:
|
||||||
# Include the device's display name in the "unsigned" dictionary
|
# Include the device's display name in the "unsigned" dictionary
|
||||||
display_name = device_info.display_name
|
display_name = device_info.display_name
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
|
from copy import deepcopy
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
@ -205,6 +206,141 @@ class KeyQueryTestCase(unittest.HomeserverTestCase):
|
||||||
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
|
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
|
||||||
|
|
||||||
|
|
||||||
|
class UnsignedKeyDataTestCase(unittest.HomeserverTestCase):
|
||||||
|
servlets = [
|
||||||
|
keys.register_servlets,
|
||||||
|
admin.register_servlets_for_client_rest_resource,
|
||||||
|
login.register_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def default_config(self) -> JsonDict:
|
||||||
|
config = super().default_config()
|
||||||
|
config["experimental_features"] = {"msc4229_enabled": True}
|
||||||
|
return config
|
||||||
|
|
||||||
|
def make_key_data(self, user_id: str, device_id: str) -> JsonDict:
|
||||||
|
return {
|
||||||
|
"algorithms": ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||||
|
"device_id": device_id,
|
||||||
|
"keys": {
|
||||||
|
f"curve25519:{device_id}": "keykeykey",
|
||||||
|
f"ed25519:{device_id}": "keykeykey",
|
||||||
|
},
|
||||||
|
"signatures": {user_id: {f"ed25519:{device_id}": "sigsigsig"}},
|
||||||
|
"user_id": user_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_unsigned_uploaded_data_returned_in_keys_query(self) -> None:
|
||||||
|
password = "wonderland"
|
||||||
|
device_id = "ABCDEFGHI"
|
||||||
|
alice_id = self.register_user("alice", password)
|
||||||
|
alice_token = self.login(
|
||||||
|
"alice",
|
||||||
|
password,
|
||||||
|
device_id=device_id,
|
||||||
|
additional_request_fields={"initial_device_display_name": "mydevice"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Alice uploads some keys, with a bit of unsigned data
|
||||||
|
keys1 = self.make_key_data(alice_id, device_id)
|
||||||
|
keys1["unsigned"] = {"a": "b"}
|
||||||
|
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
"/_matrix/client/v3/keys/upload",
|
||||||
|
{"device_keys": keys1},
|
||||||
|
alice_token,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
|
||||||
|
|
||||||
|
# /keys/query should return the unsigned data, with the device display name merged in.
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
"/_matrix/client/v3/keys/query",
|
||||||
|
{"device_keys": {alice_id: []}},
|
||||||
|
alice_token,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
|
||||||
|
device_response = channel.json_body["device_keys"][alice_id][device_id]
|
||||||
|
expected_device_response = deepcopy(keys1)
|
||||||
|
expected_device_response["unsigned"]["device_display_name"] = "mydevice"
|
||||||
|
self.assertEqual(device_response, expected_device_response)
|
||||||
|
|
||||||
|
# /_matrix/federation/v1/user/devices/{userId} should return the unsigned data too
|
||||||
|
fed_response = self.get_success(
|
||||||
|
self.hs.get_device_handler().on_federation_query_user_devices(alice_id)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
fed_response["devices"][0],
|
||||||
|
{"device_id": device_id, "keys": keys1},
|
||||||
|
)
|
||||||
|
|
||||||
|
# so should /_matrix/federation/v1/user/keys/query
|
||||||
|
fed_response = self.get_success(
|
||||||
|
self.hs.get_e2e_keys_handler().on_federation_query_client_keys(
|
||||||
|
{"device_keys": {alice_id: []}}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fed_device_response = fed_response["device_keys"][alice_id][device_id]
|
||||||
|
self.assertEqual(fed_device_response, keys1)
|
||||||
|
|
||||||
|
def test_non_dict_unsigned_is_ignored(self) -> None:
|
||||||
|
password = "wonderland"
|
||||||
|
device_id = "ABCDEFGHI"
|
||||||
|
alice_id = self.register_user("alice", password)
|
||||||
|
alice_token = self.login(
|
||||||
|
"alice",
|
||||||
|
password,
|
||||||
|
device_id=device_id,
|
||||||
|
additional_request_fields={"initial_device_display_name": "mydevice"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Alice uploads some keys, with a malformed unsigned data
|
||||||
|
keys1 = self.make_key_data(alice_id, device_id)
|
||||||
|
keys1["unsigned"] = ["a", "b"] # a list!
|
||||||
|
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
"/_matrix/client/v3/keys/upload",
|
||||||
|
{"device_keys": keys1},
|
||||||
|
alice_token,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
|
||||||
|
|
||||||
|
# /keys/query should return the unsigned data, with the device display name merged in.
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
"/_matrix/client/v3/keys/query",
|
||||||
|
{"device_keys": {alice_id: []}},
|
||||||
|
alice_token,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, HTTPStatus.OK, channel.result)
|
||||||
|
device_response = channel.json_body["device_keys"][alice_id][device_id]
|
||||||
|
expected_device_response = deepcopy(keys1)
|
||||||
|
expected_device_response["unsigned"] = {"device_display_name": "mydevice"}
|
||||||
|
self.assertEqual(device_response, expected_device_response)
|
||||||
|
|
||||||
|
# /_matrix/federation/v1/user/devices/{userId} should return the unsigned data too
|
||||||
|
fed_response = self.get_success(
|
||||||
|
self.hs.get_device_handler().on_federation_query_user_devices(alice_id)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
fed_response["devices"][0],
|
||||||
|
{"device_id": device_id, "keys": keys1},
|
||||||
|
)
|
||||||
|
|
||||||
|
# so should /_matrix/federation/v1/user/keys/query
|
||||||
|
fed_response = self.get_success(
|
||||||
|
self.hs.get_e2e_keys_handler().on_federation_query_client_keys(
|
||||||
|
{"device_keys": {alice_id: []}}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fed_device_response = fed_response["device_keys"][alice_id][device_id]
|
||||||
|
expected_device_response = deepcopy(keys1)
|
||||||
|
expected_device_response["unsigned"] = {}
|
||||||
|
self.assertEqual(fed_device_response, expected_device_response)
|
||||||
|
|
||||||
|
|
||||||
class SigningKeyUploadServletTestCase(unittest.HomeserverTestCase):
|
class SigningKeyUploadServletTestCase(unittest.HomeserverTestCase):
|
||||||
servlets = [
|
servlets = [
|
||||||
admin.register_servlets,
|
admin.register_servlets,
|
||||||
|
|
Loading…
Reference in a new issue