Skip to content

Commit c6cf87e

Browse files
anoadragon453phil-flex
authored andcommitted
Query missing cross-signing keys on local sig upload (matrix-org#7289)
1 parent 0d69d29 commit c6cf87e

File tree

3 files changed

+180
-18
lines changed

3 files changed

+180
-18
lines changed

changelog.d/7289.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a bug with cross-signing devices belonging to remote users who did not share a room with any user on the local homeserver.

synapse/federation/transport/client.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -399,20 +399,30 @@ def query_client_keys(self, destination, query_content, timeout):
399399
{
400400
"device_keys": {
401401
"<user_id>": ["<device_id>"]
402-
} }
402+
}
403+
}
403404
404405
Response:
405406
{
406407
"device_keys": {
407408
"<user_id>": {
408409
"<device_id>": {...}
409-
} } }
410+
}
411+
},
412+
"master_key": {
413+
"<user_id>": {...}
414+
}
415+
},
416+
"self_signing_key": {
417+
"<user_id>": {...}
418+
}
419+
}
410420
411421
Args:
412422
destination(str): The server to query.
413423
query_content(dict): The user ids to query.
414424
Returns:
415-
A dict containg the device keys.
425+
A dict containing device and cross-signing keys.
416426
"""
417427
path = _create_v1_path("/user/keys/query")
418428

@@ -429,14 +439,30 @@ def query_user_devices(self, destination, user_id, timeout):
429439
Response:
430440
{
431441
"stream_id": "...",
432-
"devices": [ { ... } ]
442+
"devices": [ { ... } ],
443+
"master_key": {
444+
"user_id": "<user_id>",
445+
"usage": [...],
446+
"keys": {...},
447+
"signatures": {
448+
"<user_id>": {...}
449+
}
450+
},
451+
"self_signing_key": {
452+
"user_id": "<user_id>",
453+
"usage": [...],
454+
"keys": {...},
455+
"signatures": {
456+
"<user_id>": {...}
457+
}
458+
}
433459
}
434460
435461
Args:
436462
destination(str): The server to query.
437463
query_content(dict): The user ids to query.
438464
Returns:
439-
A dict containg the device keys.
465+
A dict containing device and cross-signing keys.
440466
"""
441467
path = _create_v1_path("/user/devices/%s", user_id)
442468

@@ -454,22 +480,27 @@ def claim_client_keys(self, destination, query_content, timeout):
454480
{
455481
"one_time_keys": {
456482
"<user_id>": {
457-
"<device_id>": "<algorithm>"
458-
} } }
483+
"<device_id>": "<algorithm>"
484+
}
485+
}
486+
}
459487
460488
Response:
461489
{
462490
"device_keys": {
463491
"<user_id>": {
464492
"<device_id>": {
465493
"<algorithm>:<key_id>": "<key_base64>"
466-
} } } }
494+
}
495+
}
496+
}
497+
}
467498
468499
Args:
469500
destination(str): The server to query.
470501
query_content(dict): The user ids to query.
471502
Returns:
472-
A dict containg the one-time keys.
503+
A dict containing the one-time keys.
473504
"""
474505

475506
path = _create_v1_path("/user/keys/claim")

synapse/handlers/e2e_keys.py

Lines changed: 139 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,8 @@ def do_remote_query(destination):
174174
"""This is called when we are querying the device list of a user on
175175
a remote homeserver and their device list is not in the device list
176176
cache. If we share a room with this user and we're not querying for
177-
specific user we will update the cache
178-
with their device list."""
177+
specific user we will update the cache with their device list.
178+
"""
179179

180180
destination_query = remote_queries_not_in_cache[destination]
181181

@@ -961,13 +961,19 @@ def _process_other_signatures(self, user_id, signatures):
961961
return signature_list, failures
962962

963963
@defer.inlineCallbacks
964-
def _get_e2e_cross_signing_verify_key(self, user_id, key_type, from_user_id=None):
965-
"""Fetch the cross-signing public key from storage and interpret it.
964+
def _get_e2e_cross_signing_verify_key(
965+
self, user_id: str, key_type: str, from_user_id: str = None
966+
):
967+
"""Fetch locally or remotely query for a cross-signing public key.
968+
969+
First, attempt to fetch the cross-signing public key from storage.
970+
If that fails, query the keys from the homeserver they belong to
971+
and update our local copy.
966972
967973
Args:
968-
user_id (str): the user whose key should be fetched
969-
key_type (str): the type of key to fetch
970-
from_user_id (str): the user that we are fetching the keys for.
974+
user_id: the user whose key should be fetched
975+
key_type: the type of key to fetch
976+
from_user_id: the user that we are fetching the keys for.
971977
This affects what signatures are fetched.
972978
973979
Returns:
@@ -976,16 +982,140 @@ def _get_e2e_cross_signing_verify_key(self, user_id, key_type, from_user_id=None
976982
977983
Raises:
978984
NotFoundError: if the key is not found
985+
SynapseError: if `user_id` is invalid
979986
"""
987+
user = UserID.from_string(user_id)
980988
key = yield self.store.get_e2e_cross_signing_key(
981989
user_id, key_type, from_user_id
982990
)
991+
992+
if key:
993+
# We found a copy of this key in our database. Decode and return it
994+
key_id, verify_key = get_verify_key_from_cross_signing_key(key)
995+
return key, key_id, verify_key
996+
997+
# If we couldn't find the key locally, and we're looking for keys of
998+
# another user then attempt to fetch the missing key from the remote
999+
# user's server.
1000+
#
1001+
# We may run into this in possible edge cases where a user tries to
1002+
# cross-sign a remote user, but does not share any rooms with them yet.
1003+
# Thus, we would not have their key list yet. We instead fetch the key,
1004+
# store it and notify clients of new, associated device IDs.
1005+
if self.is_mine(user) or key_type not in ["master", "self_signing"]:
1006+
# Note that master and self_signing keys are the only cross-signing keys we
1007+
# can request over federation
1008+
raise NotFoundError("No %s key found for %s" % (key_type, user_id))
1009+
1010+
(
1011+
key,
1012+
key_id,
1013+
verify_key,
1014+
) = yield self._retrieve_cross_signing_keys_for_remote_user(user, key_type)
1015+
9831016
if key is None:
984-
logger.debug("no %s key found for %s", key_type, user_id)
9851017
raise NotFoundError("No %s key found for %s" % (key_type, user_id))
986-
key_id, verify_key = get_verify_key_from_cross_signing_key(key)
1018+
9871019
return key, key_id, verify_key
9881020

1021+
@defer.inlineCallbacks
1022+
def _retrieve_cross_signing_keys_for_remote_user(
1023+
self, user: UserID, desired_key_type: str,
1024+
):
1025+
"""Queries cross-signing keys for a remote user and saves them to the database
1026+
1027+
Only the key specified by `key_type` will be returned, while all retrieved keys
1028+
will be saved regardless
1029+
1030+
Args:
1031+
user: The user to query remote keys for
1032+
desired_key_type: The type of key to receive. One of "master", "self_signing"
1033+
1034+
Returns:
1035+
Deferred[Tuple[Optional[Dict], Optional[str], Optional[VerifyKey]]]: A tuple
1036+
of the retrieved key content, the key's ID and the matching VerifyKey.
1037+
If the key cannot be retrieved, all values in the tuple will instead be None.
1038+
"""
1039+
try:
1040+
remote_result = yield self.federation.query_user_devices(
1041+
user.domain, user.to_string()
1042+
)
1043+
except Exception as e:
1044+
logger.warning(
1045+
"Unable to query %s for cross-signing keys of user %s: %s %s",
1046+
user.domain,
1047+
user.to_string(),
1048+
type(e),
1049+
e,
1050+
)
1051+
return None, None, None
1052+
1053+
# Process each of the retrieved cross-signing keys
1054+
desired_key = None
1055+
desired_key_id = None
1056+
desired_verify_key = None
1057+
retrieved_device_ids = []
1058+
for key_type in ["master", "self_signing"]:
1059+
key_content = remote_result.get(key_type + "_key")
1060+
if not key_content:
1061+
continue
1062+
1063+
# Ensure these keys belong to the correct user
1064+
if "user_id" not in key_content:
1065+
logger.warning(
1066+
"Invalid %s key retrieved, missing user_id field: %s",
1067+
key_type,
1068+
key_content,
1069+
)
1070+
continue
1071+
if user.to_string() != key_content["user_id"]:
1072+
logger.warning(
1073+
"Found %s key of user %s when querying for keys of user %s",
1074+
key_type,
1075+
key_content["user_id"],
1076+
user.to_string(),
1077+
)
1078+
continue
1079+
1080+
# Validate the key contents
1081+
try:
1082+
# verify_key is a VerifyKey from signedjson, which uses
1083+
# .version to denote the portion of the key ID after the
1084+
# algorithm and colon, which is the device ID
1085+
key_id, verify_key = get_verify_key_from_cross_signing_key(key_content)
1086+
except ValueError as e:
1087+
logger.warning(
1088+
"Invalid %s key retrieved: %s - %s %s",
1089+
key_type,
1090+
key_content,
1091+
type(e),
1092+
e,
1093+
)
1094+
continue
1095+
1096+
# Note down the device ID attached to this key
1097+
retrieved_device_ids.append(verify_key.version)
1098+
1099+
# If this is the desired key type, save it and its ID/VerifyKey
1100+
if key_type == desired_key_type:
1101+
desired_key = key_content
1102+
desired_verify_key = verify_key
1103+
desired_key_id = key_id
1104+
1105+
# At the same time, store this key in the db for subsequent queries
1106+
yield self.store.set_e2e_cross_signing_key(
1107+
user.to_string(), key_type, key_content
1108+
)
1109+
1110+
# Notify clients that new devices for this user have been discovered
1111+
if retrieved_device_ids:
1112+
# XXX is this necessary?
1113+
yield self.device_handler.notify_device_update(
1114+
user.to_string(), retrieved_device_ids
1115+
)
1116+
1117+
return desired_key, desired_key_id, desired_verify_key
1118+
9891119

9901120
def _check_cross_signing_key(key, user_id, key_type, signing_key=None):
9911121
"""Check a cross-signing key uploaded by a user. Performs some basic sanity

0 commit comments

Comments
 (0)