Include cross-signing signatures when syncing remote devices for the first time (#11234)

When fetching remote devices for the first time, we did not correctly include the cross signing keys in the returned results.

c.f. #11159
This commit is contained in:
Erik Johnston 2021-11-09 11:45:36 +00:00 committed by GitHub
parent 820337e6a4
commit af784644c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 277 additions and 86 deletions

1
changelog.d/11234.bugfix Normal file
View File

@ -0,0 +1 @@
Fix long-standing bug where cross signing keys were not included in the response to `/r0/keys/query` the first time a remote user was queried.

View File

@ -201,15 +201,57 @@ class E2eKeysHandler:
r[user_id] = remote_queries[user_id] r[user_id] = remote_queries[user_id]
# Now fetch any devices that we don't have in our cache # Now fetch any devices that we don't have in our cache
await make_deferred_yieldable(
defer.gatherResults(
[
run_in_background(
self._query_devices_for_destination,
results,
cross_signing_keys,
failures,
destination,
queries,
timeout,
)
for destination, queries in remote_queries_not_in_cache.items()
],
consumeErrors=True,
).addErrback(unwrapFirstError)
)
ret = {"device_keys": results, "failures": failures}
ret.update(cross_signing_keys)
return ret
@trace @trace
async def do_remote_query(destination: str) -> None: async def _query_devices_for_destination(
self,
results: JsonDict,
cross_signing_keys: JsonDict,
failures: Dict[str, JsonDict],
destination: str,
destination_query: Dict[str, Iterable[str]],
timeout: int,
) -> None:
"""This is called when we are querying the device list of a user on """This is called when we are querying the device list of a user on
a remote homeserver and their device list is not in the device list a remote homeserver and their device list is not in the device list
cache. If we share a room with this user and we're not querying for cache. If we share a room with this user and we're not querying for
specific user we will update the cache with their device list. specific user we will update the cache with their device list.
"""
destination_query = remote_queries_not_in_cache[destination] Args:
results: A map from user ID to their device keys, which gets
updated with the newly fetched keys.
cross_signing_keys: Map from user ID to their cross signing keys,
which gets updated with the newly fetched keys.
failures: Map of destinations to failures that have occurred while
attempting to fetch keys.
destination: The remote server to query
destination_query: The query dict of devices to query the remote
server for.
timeout: The timeout for remote HTTP requests.
"""
# We first consider whether we wish to update the device list cache with # We first consider whether we wish to update the device list cache with
# the users device list. We want to track a user's devices when the # the users device list. We want to track a user's devices when the
@ -235,19 +277,30 @@ class E2eKeysHandler:
# done an initial sync on the device list so we do it now. # done an initial sync on the device list so we do it now.
try: try:
if self._is_master: if self._is_master:
user_devices = await self.device_handler.device_list_updater.user_device_resync( resync_results = await self.device_handler.device_list_updater.user_device_resync(
user_id user_id
) )
else: else:
user_devices = await self._user_device_resync_client( resync_results = await self._user_device_resync_client(
user_id=user_id user_id=user_id
) )
user_devices = user_devices["devices"] # Add the device keys to the results.
user_devices = resync_results["devices"]
user_results = results.setdefault(user_id, {}) user_results = results.setdefault(user_id, {})
for device in user_devices: for device in user_devices:
user_results[device["device_id"]] = device["keys"] user_results[device["device_id"]] = device["keys"]
user_ids_updated.append(user_id) user_ids_updated.append(user_id)
# Add any cross signing keys to the results.
master_key = resync_results.get("master_key")
self_signing_key = resync_results.get("self_signing_key")
if master_key:
cross_signing_keys["master_keys"][user_id] = master_key
if self_signing_key:
cross_signing_keys["self_signing_keys"][user_id] = self_signing_key
except Exception as e: except Exception as e:
failures[destination] = _exception_to_failure(e) failures[destination] = _exception_to_failure(e)
@ -285,21 +338,7 @@ class E2eKeysHandler:
set_tag("error", True) set_tag("error", True)
set_tag("reason", failure) set_tag("reason", failure)
await make_deferred_yieldable( return
defer.gatherResults(
[
run_in_background(do_remote_query, destination)
for destination in remote_queries_not_in_cache
],
consumeErrors=True,
).addErrback(unwrapFirstError)
)
ret = {"device_keys": results, "failures": failures}
ret.update(cross_signing_keys)
return ret
async def get_cross_signing_keys_from_cache( async def get_cross_signing_keys_from_cache(
self, query: Iterable[str], from_user_id: Optional[str] self, query: Iterable[str], from_user_id: Optional[str]

View File

@ -17,6 +17,8 @@ from unittest import mock
from signedjson import key as key, sign as sign from signedjson import key as key, sign as sign
from twisted.internet import defer
from synapse.api.constants import RoomEncryptionAlgorithms from synapse.api.constants import RoomEncryptionAlgorithms
from synapse.api.errors import Codes, SynapseError from synapse.api.errors import Codes, SynapseError
@ -630,3 +632,152 @@ class E2eKeysHandlerTestCase(unittest.HomeserverTestCase):
], ],
other_master_key["signatures"][local_user]["ed25519:" + usersigning_pubkey], other_master_key["signatures"][local_user]["ed25519:" + usersigning_pubkey],
) )
def test_query_devices_remote_no_sync(self):
"""Tests that querying keys for a remote user that we don't share a room
with returns the cross signing keys correctly.
"""
remote_user_id = "@test:other"
local_user_id = "@test:test"
remote_master_key = "85T7JXPFBAySB/jwby4S3lBPTqY3+Zg53nYuGmu1ggY"
remote_self_signing_key = "QeIiFEjluPBtI7WQdG365QKZcFs9kqmHir6RBD0//nQ"
self.hs.get_federation_client().query_client_keys = mock.Mock(
return_value=defer.succeed(
{
"device_keys": {remote_user_id: {}},
"master_keys": {
remote_user_id: {
"user_id": remote_user_id,
"usage": ["master"],
"keys": {"ed25519:" + remote_master_key: remote_master_key},
},
},
"self_signing_keys": {
remote_user_id: {
"user_id": remote_user_id,
"usage": ["self_signing"],
"keys": {
"ed25519:"
+ remote_self_signing_key: remote_self_signing_key
},
}
},
}
)
)
e2e_handler = self.hs.get_e2e_keys_handler()
query_result = self.get_success(
e2e_handler.query_devices(
{
"device_keys": {remote_user_id: []},
},
timeout=10,
from_user_id=local_user_id,
from_device_id="some_device_id",
)
)
self.assertEqual(query_result["failures"], {})
self.assertEqual(
query_result["master_keys"],
{
remote_user_id: {
"user_id": remote_user_id,
"usage": ["master"],
"keys": {"ed25519:" + remote_master_key: remote_master_key},
},
},
)
self.assertEqual(
query_result["self_signing_keys"],
{
remote_user_id: {
"user_id": remote_user_id,
"usage": ["self_signing"],
"keys": {
"ed25519:" + remote_self_signing_key: remote_self_signing_key
},
}
},
)
def test_query_devices_remote_sync(self):
"""Tests that querying keys for a remote user that we share a room with,
but haven't yet fetched the keys for, returns the cross signing keys
correctly.
"""
remote_user_id = "@test:other"
local_user_id = "@test:test"
self.store.get_rooms_for_user = mock.Mock(
return_value=defer.succeed({"some_room_id"})
)
remote_master_key = "85T7JXPFBAySB/jwby4S3lBPTqY3+Zg53nYuGmu1ggY"
remote_self_signing_key = "QeIiFEjluPBtI7WQdG365QKZcFs9kqmHir6RBD0//nQ"
self.hs.get_federation_client().query_user_devices = mock.Mock(
return_value=defer.succeed(
{
"user_id": remote_user_id,
"stream_id": 1,
"devices": [],
"master_key": {
"user_id": remote_user_id,
"usage": ["master"],
"keys": {"ed25519:" + remote_master_key: remote_master_key},
},
"self_signing_key": {
"user_id": remote_user_id,
"usage": ["self_signing"],
"keys": {
"ed25519:"
+ remote_self_signing_key: remote_self_signing_key
},
},
}
)
)
e2e_handler = self.hs.get_e2e_keys_handler()
query_result = self.get_success(
e2e_handler.query_devices(
{
"device_keys": {remote_user_id: []},
},
timeout=10,
from_user_id=local_user_id,
from_device_id="some_device_id",
)
)
self.assertEqual(query_result["failures"], {})
self.assertEqual(
query_result["master_keys"],
{
remote_user_id: {
"user_id": remote_user_id,
"usage": ["master"],
"keys": {"ed25519:" + remote_master_key: remote_master_key},
}
},
)
self.assertEqual(
query_result["self_signing_keys"],
{
remote_user_id: {
"user_id": remote_user_id,
"usage": ["self_signing"],
"keys": {
"ed25519:" + remote_self_signing_key: remote_self_signing_key
},
}
},
)