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

Query missing cross-signing keys on local sig upload #7289

Merged
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8348481
Query missing cross-signing keys on local sig upload
anoadragon453 Apr 16, 2020
1063495
Add changelog
anoadragon453 Apr 16, 2020
cc86457
Save retrieved keys to the db
anoadragon453 Apr 16, 2020
c265bc7
lint
anoadragon453 Apr 16, 2020
39ed9f6
Fix and de-brittle remote result dict processing
anoadragon453 Apr 16, 2020
fd8d154
Use query_user_devices instead, assume only master, self_signing key …
anoadragon453 Apr 16, 2020
759b6b0
Make changelog more useful
anoadragon453 Apr 16, 2020
03d2c8c
Remove very specific exception handling
anoadragon453 Apr 16, 2020
b386658
Wrap get_verify_key_from_cross_signing_key in a try/except
anoadragon453 Apr 16, 2020
bd9a671
Note that _get_e2e_cross_signing_verify_key can raise a SynapseError
anoadragon453 Apr 16, 2020
745e653
lint
anoadragon453 Apr 16, 2020
f8b6f14
Add comment explaining why this is useful
anoadragon453 Apr 16, 2020
37ae643
Only fetch master and self_signing key types
anoadragon453 Apr 16, 2020
83861c3
Fix log statements, docstrings
anoadragon453 Apr 17, 2020
671178b
Remove extraneous items from remote query try/except
anoadragon453 Apr 17, 2020
2d88b5d
lint
anoadragon453 Apr 17, 2020
f417300
Factor key retrieval out into a separate function
anoadragon453 Apr 17, 2020
2f87051
Send device updates, modeled after SigningKeyEduUpdater._handle_signi…
anoadragon453 Apr 17, 2020
5990d1c
Update method docstring
anoadragon453 Apr 17, 2020
4f8ba5c
Remove extraneous key_id and verify_key
anoadragon453 Apr 20, 2020
9240abc
Update changelog
anoadragon453 Apr 20, 2020
3282423
Update changelog
anoadragon453 Apr 20, 2020
95dd9d5
Resolve review comments
anoadragon453 Apr 20, 2020
4f41f37
lint
anoadragon453 Apr 20, 2020
6d559ba
Update changelog.d/7289.bugfix
anoadragon453 Apr 21, 2020
1b4dda5
Refactor _get_e2e_cross_signing_verify_key
anoadragon453 Apr 21, 2020
7cb1e48
Refactor and add validation to _retrieve_cross_signing_keys_for_remot…
anoadragon453 Apr 21, 2020
74eaac0
Improve details of query_client_keys and query_user_devices docstrings
anoadragon453 Apr 21, 2020
b08b7c7
lint
anoadragon453 Apr 21, 2020
de29d1f
Merge branch 'anoa/query_cross_signing_keys_key_upload' of github.com…
anoadragon453 Apr 21, 2020
8484a72
Address review comments
anoadragon453 Apr 22, 2020
2932b9b
JSON brace endings on separate lines
anoadragon453 Apr 22, 2020
ebea2ee
Spaces and braces
anoadragon453 Apr 22, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/7289.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a bug with cross-signing devices with remote users when they did not share a room with any user on the local homeserver.
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
14 changes: 11 additions & 3 deletions synapse/federation/transport/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,13 +406,19 @@ def query_client_keys(self, destination, query_content, timeout):
"device_keys": {
"<user_id>": {
"<device_id>": {...}
} }
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
"master_keys": {
"<user_id>": {...}
} }
"self_signing_keys": {
"<user_id>": {...}
} } }

Args:
destination(str): The server to query.
query_content(dict): The user ids to query.
Returns:
A dict containg the device keys.
A dict containing device and cross-signing keys.
"""
path = _create_v1_path("/user/keys/query")

Expand All @@ -429,14 +435,16 @@ def query_user_devices(self, destination, user_id, timeout):
Response:
{
"stream_id": "...",
"devices": [ { ... } ]
"devices": [ { ... } ],
"master_key": { ... },
"self_signing_key: { ... }
}

Args:
destination(str): The server to query.
query_content(dict): The user ids to query.
Returns:
A dict containg the device keys.
A dict containing device and cross-signing keys.
"""
path = _create_v1_path("/user/devices/%s", user_id)

Expand Down
134 changes: 125 additions & 9 deletions synapse/handlers/e2e_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@ def do_remote_query(destination):
"""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
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.
richvdh marked this conversation as resolved.
Show resolved Hide resolved
"""

destination_query = remote_queries_not_in_cache[destination]

Expand Down Expand Up @@ -961,13 +961,19 @@ def _process_other_signatures(self, user_id, signatures):
return signature_list, failures

@defer.inlineCallbacks
def _get_e2e_cross_signing_verify_key(self, user_id, key_type, from_user_id=None):
"""Fetch the cross-signing public key from storage and interpret it.
def _get_e2e_cross_signing_verify_key(
self, user_id: str, key_type: str, from_user_id: str = None
):
"""Fetch locally or remotely query for a cross-signing public key.

First, attempt to fetch the cross-signing public key from storage.
If that fails, query the keys from the homeserver they belong to
and update our local copy.

Args:
user_id (str): the user whose key should be fetched
key_type (str): the type of key to fetch
from_user_id (str): the user that we are fetching the keys for.
user_id: the user whose key should be fetched
key_type: the type of key to fetch
from_user_id: the user that we are fetching the keys for.
This affects what signatures are fetched.

Returns:
Expand All @@ -976,16 +982,126 @@ def _get_e2e_cross_signing_verify_key(self, user_id, key_type, from_user_id=None

Raises:
NotFoundError: if the key is not found
SynapseError: if `user_id` is invalid
"""
user = UserID.from_string(user_id)
richvdh marked this conversation as resolved.
Show resolved Hide resolved
key = yield self.store.get_e2e_cross_signing_key(
user_id, key_type, from_user_id
)

# If we couldn't find the key locally, and we're looking for keys of
# another user then attempt to fetch the missing key from the remote
# user's server.
#
# We may run into this in possible edge cases where a user tries to
# cross-sign a remote user, but does not share any rooms with them yet.
# Thus, we would not have their key list yet. We fetch the key here,
# store it and notify clients of new, associated device IDs.
if (
key is None
and not self.is_mine(user)
# We only get "master" and "self_signing" keys from remote servers
and key_type in ["master", "self_signing"]
):
(
key,
key_id,
verify_key,
) = yield self._retrieve_cross_signing_keys_for_remote_user(user, key_type)

if key is None:
logger.debug("no %s key found for %s", key_type, user_id)
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
logger.warning("No %s key found for %s", key_type, user_id)
raise NotFoundError("No %s key found for %s" % (key_type, user_id))
key_id, verify_key = get_verify_key_from_cross_signing_key(key)

try:
key_id, verify_key = get_verify_key_from_cross_signing_key(key)
except ValueError as e:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid this still feels like spaghetti. Why are we returning key_id and verify_key from _retrieve_cross_signing_keys_for_remote_user if we immediately throw them away?

how about:

        key = yield self.store.get_e2e_cross_signing_key(
            user_id, key_type, from_user_id
        )

        if key:
            # we found a copy of this key in our database. decode it and return it.
            # XXX is a try/except needed here? If so, why? we didn't have one before.
            key_id, verify_key = get_verify_key_from_cross_signing_key(key)
            return key, key_id, verify_key

        # some words about the edge case
        if not self.is_mine(user) and key_type in ["master", "self_signing"]:
            (
                key,
                key_id,
                verify_key,
            ) = yield self._retrieve_cross_signing_keys_for_remote_user(user, key_type)

        if key is None:
            logger.debug("No %s key found for %s", key_type, user_id)   # XXX or wrning?
            raise NotFoundError("No %s key found for %s" % (key_type, user_id))

        return key, key_id, verify_key

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XXX is a try/except needed here? If so, why? we didn't have one before.

I suppose one wouldn't be if we trust the data in our database, which we probably should be.

logger.warning(
"Invalid %s key retrieved: %s - %s %s", key_type, key, type(e), e,
)
raise SynapseError(
502, "Invalid %s key retrieved from remote server" % (key_type,)
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
)

return key, key_id, verify_key

@defer.inlineCallbacks
def _retrieve_cross_signing_keys_for_remote_user(
self, user: UserID, desired_key_type: str,
):
"""Queries cross-signing keys for a remote user and saves them to the database

Only the key specified by `key_type` will be returned, while all retrieved keys
will be saved regardless

Args:
user: The user to query remote keys for
desired_key_type: The type of key to receive. One of "master", "self_signing"

Returns:
Deferred[Tuple[Optional[Dict], Optional[str], Optional[VerifyKey]]]: A tuple
of the retrieved key content, the key's ID and the matching VerifyKey.
If the key cannot be retrieved, all values in the tuple will instead be None.
"""
try:
remote_result = yield self.federation.query_user_devices(
user.domain, user.to_string()
)
except Exception as e:
logger.warning(
"Unable to query %s for cross-signing keys of user %s: %s %s",
user.domain,
user.to_string(),
type(e),
e,
)
return None, None, None

# Process each of the retrieved cross-signing keys
final_key = None
final_key_id = None
final_verify_key = None
device_ids = []
for key_type in ["master", "self_signing"]:
key_content = remote_result.get(key_type + "_key")
if not key_content:
continue

# At the same time, store this key in the db for
# subsequent queries
yield self.store.set_e2e_cross_signing_key(
user.to_string(), key_type, key_content
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it correct that we store it before we check that it can be parsed? (In short, is the data in the table supposed to have been validated or not? If not, we need to be careful when we extract it). What does the existing code do?

Copy link
Member Author

@anoadragon453 anoadragon453 Apr 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering the above call from the database did not have a try/except, I believe we're operating on the assumption that the data in the database is valid.

Yes, we should validate this data in some way. Some level of such is done in do_remote_query:

for user_id, keys in remote_result["device_keys"].items():
if user_id in destination_query:
results[user_id] = keys
if "master_keys" in remote_result:
for user_id, key in remote_result["master_keys"].items():
if user_id in destination_query:
cross_signing_keys["master_keys"][user_id] = key
if "self_signing_keys" in remote_result:
for user_id, key in remote_result["self_signing_keys"].items():
if user_id in destination_query:
cross_signing_keys["self_signing_keys"][user_id] = key

I'd hate to duplicate but that could be lifted and modified.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Speaking of verification, one thing that's confusing me is that in the loop of _retrieve_cross_signing_keys_for_remote_user, remote_result should look something like this:

Response:
{
"device_keys": {
"<user_id>": {
"<device_id>": {...}
} }
"master_keys": {
"<user_id>": {...}
} }
"self_signing_keys": {
"<user_id>": {...}
} } }

Thus key_content should look something like:

"<user_id>": {
"<device_id>": {...}

And thus we should need to extract the key_contents from that dictionary before we pass it to get_verify_key_from_cross_signing_key.

And yet this PR works, so something isn't lining up here.

Copy link
Member Author

@anoadragon453 anoadragon453 Apr 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already, so my docstring update is incorrect, as the thing I actually get back from query_client_keys is in the form:

{
  "user_id": "@test18:workerstest.amorgan.xyz",
  "stream_id": 213,
  "devices": [
    {
      "device_id": "MLYKYOAKEZ"
    },
    {
      "device_id": "NBICDLRVHV",
      "keys": {
        "algorithms": [
          "m.olm.v1.curve25519-aes-sha2",
          "m.megolm.v1.aes-sha2"
        ],
        "device_id": "NBICDLRVHV",
        "keys": {
          "curve25519:NBICDLRVHV": "E9ryKjylPorQIxyRA4FsJwi9d2bPECLvWKjgcuqmamU",
          "ed25519:NBICDLRVHV": "yk5AMkCyhogBCQt1CrVbFL9weRb/5iExmIDYd7JmBhE"
        },
        "signatures": {
          "@test18:workerstest.amorgan.xyz": {
            "ed25519:NBICDLRVHV": "JTAL+uIduDDJjdQneocTbjqjIDMljqDOq8bJ6ZgAYOQ01wMHrX4qRmQufQT9KAiYoXQ+b6UY+ZNEIFjyi1rjCg",
            "ed25519:cEYsFkRyHrfj1fLLgq4rA+JyvOwHda2mO092N4scL/E": "RQSm3aYjvWMWHx0GHZPRRKvj90TEdlqjOGXulXBUSDEuI2P64A7N/7/uSdx//BhqOgcmZSS/9o1Awlpb+KiiCg"
          }
        },
        "user_id": "@test18:workerstest.amorgan.xyz"
      },
      "device_display_name": "https://riot.im/develop/ via Firefox on Linux"
    }
  ],
  "master_key": {
    "user_id": "@test18:workerstest.amorgan.xyz",
    "usage": [
      "master"
    ],
    "keys": {
      "ed25519:RPg3wmdeZOvjMCmi3/pP9+4ndSW9W2dhE/RkLyCdr+E": "RPg3wmdeZOvjMCmi3/pP9+4ndSW9W2dhE/RkLyCdr+E"
    },
    "signatures": {
      "@test18:workerstest.amorgan.xyz": {
        "ed25519:NBICDLRVHV": "YLdKUD4aDmJx/vqIWjbqXTOZXv+cU1ba8Z9hw44vVn32uhLdJAiGC+L/6HeBTz7+Wh+NdWGBiy+usea3408bDA"
      }
    }
  },
  "self_signing_key": {
    "user_id": "@test18:workerstest.amorgan.xyz",
    "usage": [
      "self_signing"
    ],
    "keys": {
      "ed25519:cEYsFkRyHrfj1fLLgq4rA+JyvOwHda2mO092N4scL/E": "cEYsFkRyHrfj1fLLgq4rA+JyvOwHda2mO092N4scL/E"
    },
    "signatures": {
      "@test18:workerstest.amorgan.xyz": {
        "ed25519:RPg3wmdeZOvjMCmi3/pP9+4ndSW9W2dhE/RkLyCdr+E": "1H9aaL9623L764uGAezZM5jH3Gp5DLwto4s9rz+IP3bGQqLo0Cyi2bWK54aJbVrJ6akwxahZRs/n9vLDn3nIDQ"
      }
    }
  }
}

Lemme update that.

In terms of verification, I think we just need to check the user_id matches the user we're querying for, and that there's a keys field under each *_keys key.

Copy link
Member Author

@anoadragon453 anoadragon453 Apr 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aaand I was looking at the output of query_user_devices, not query_client_keys. Have updated the right docstring now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to conclude, we now validate the key by checking it belongs to the right user, and has a valid keys key dict in it with get_verify_key_from_cross_signing_key.


# Note down the device ID attached to this key
try:
# verify_key is a VerifyKey from signedjson, which uses
# .version to denote the portion of the key ID after the
# algorithm and colon, which is the device ID
key_id, verify_key = get_verify_key_from_cross_signing_key(key_content)
except ValueError as e:
logger.warning(
"Invalid %s key retrieved: %s - %s %s",
key_type,
key_content,
type(e),
e,
)
continue
device_ids.append(verify_key.version)

# If this is the desired key type, save it and its ID/VerifyKey
if key_type == desired_key_type:
final_key = key_content
final_verify_key = verify_key
final_key_id = key_id

# Notify clients that new devices for this user have been discovered
anoadragon453 marked this conversation as resolved.
Show resolved Hide resolved
if device_ids:
yield self.device_handler.notify_device_update(user.to_string(), device_ids)

return final_key, final_key_id, final_verify_key


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