|
@@ -1,6 +1,7 @@
|
|
|
# -*- coding: utf-8 -*-
|
|
|
# Copyright 2016 OpenMarket Ltd
|
|
|
-# Copyright 2018 New Vector Ltd
|
|
|
+# Copyright 2018-2019 New Vector Ltd
|
|
|
+# Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
|
#
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
# you may not use this file except in compliance with the License.
|
|
@@ -19,13 +20,18 @@ import logging
|
|
|
from six import iteritems
|
|
|
|
|
|
from canonicaljson import encode_canonical_json, json
|
|
|
+from signedjson.sign import SignatureVerifyException, verify_signed_json
|
|
|
|
|
|
from twisted.internet import defer
|
|
|
|
|
|
-from synapse.api.errors import CodeMessageException, SynapseError
|
|
|
+from synapse.api.errors import CodeMessageException, Codes, SynapseError
|
|
|
from synapse.logging.context import make_deferred_yieldable, run_in_background
|
|
|
from synapse.logging.opentracing import log_kv, set_tag, tag_args, trace
|
|
|
-from synapse.types import UserID, get_domain_from_id
|
|
|
+from synapse.types import (
|
|
|
+ UserID,
|
|
|
+ get_domain_from_id,
|
|
|
+ get_verify_key_from_cross_signing_key,
|
|
|
+)
|
|
|
from synapse.util import unwrapFirstError
|
|
|
from synapse.util.retryutils import NotRetryingDestination
|
|
|
|
|
@@ -49,7 +55,7 @@ class E2eKeysHandler(object):
|
|
|
|
|
|
@trace
|
|
|
@defer.inlineCallbacks
|
|
|
- def query_devices(self, query_body, timeout):
|
|
|
+ def query_devices(self, query_body, timeout, from_user_id):
|
|
|
""" Handle a device key query from a client
|
|
|
|
|
|
{
|
|
@@ -67,6 +73,11 @@ class E2eKeysHandler(object):
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ Args:
|
|
|
+ from_user_id (str): the user making the query. This is used when
|
|
|
+ adding cross-signing signatures to limit what signatures users
|
|
|
+ can see.
|
|
|
"""
|
|
|
|
|
|
device_keys_query = query_body.get("device_keys", {})
|
|
@@ -125,6 +136,11 @@ class E2eKeysHandler(object):
|
|
|
r = remote_queries_not_in_cache.setdefault(domain, {})
|
|
|
r[user_id] = remote_queries[user_id]
|
|
|
|
|
|
+ # Get cached cross-signing keys
|
|
|
+ cross_signing_keys = yield self.get_cross_signing_keys_from_cache(
|
|
|
+ device_keys_query, from_user_id
|
|
|
+ )
|
|
|
+
|
|
|
# Now fetch any devices that we don't have in our cache
|
|
|
@trace
|
|
|
@defer.inlineCallbacks
|
|
@@ -188,6 +204,14 @@ class E2eKeysHandler(object):
|
|
|
if user_id in destination_query:
|
|
|
results[user_id] = keys
|
|
|
|
|
|
+ for user_id, key in remote_result["master_keys"].items():
|
|
|
+ if user_id in destination_query:
|
|
|
+ cross_signing_keys["master_keys"][user_id] = key
|
|
|
+
|
|
|
+ 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
|
|
|
+
|
|
|
except Exception as e:
|
|
|
failure = _exception_to_failure(e)
|
|
|
failures[destination] = failure
|
|
@@ -204,7 +228,61 @@ class E2eKeysHandler(object):
|
|
|
).addErrback(unwrapFirstError)
|
|
|
)
|
|
|
|
|
|
- return {"device_keys": results, "failures": failures}
|
|
|
+ ret = {"device_keys": results, "failures": failures}
|
|
|
+
|
|
|
+ ret.update(cross_signing_keys)
|
|
|
+
|
|
|
+ return ret
|
|
|
+
|
|
|
+ @defer.inlineCallbacks
|
|
|
+ def get_cross_signing_keys_from_cache(self, query, from_user_id):
|
|
|
+ """Get cross-signing keys for users from the database
|
|
|
+
|
|
|
+ Args:
|
|
|
+ query (Iterable[string]) an iterable of user IDs. A dict whose keys
|
|
|
+ are user IDs satisfies this, so the query format used for
|
|
|
+ query_devices can be used here.
|
|
|
+ from_user_id (str): the user making the query. This is used when
|
|
|
+ adding cross-signing signatures to limit what signatures users
|
|
|
+ can see.
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ defer.Deferred[dict[str, dict[str, dict]]]: map from
|
|
|
+ (master|self_signing|user_signing) -> user_id -> key
|
|
|
+ """
|
|
|
+ master_keys = {}
|
|
|
+ self_signing_keys = {}
|
|
|
+ user_signing_keys = {}
|
|
|
+
|
|
|
+ for user_id in query:
|
|
|
+ # XXX: consider changing the store functions to allow querying
|
|
|
+ # multiple users simultaneously.
|
|
|
+ key = yield self.store.get_e2e_cross_signing_key(
|
|
|
+ user_id, "master", from_user_id
|
|
|
+ )
|
|
|
+ if key:
|
|
|
+ master_keys[user_id] = key
|
|
|
+
|
|
|
+ key = yield self.store.get_e2e_cross_signing_key(
|
|
|
+ user_id, "self_signing", from_user_id
|
|
|
+ )
|
|
|
+ if key:
|
|
|
+ self_signing_keys[user_id] = key
|
|
|
+
|
|
|
+ # users can see other users' master and self-signing keys, but can
|
|
|
+ # only see their own user-signing keys
|
|
|
+ if from_user_id == user_id:
|
|
|
+ key = yield self.store.get_e2e_cross_signing_key(
|
|
|
+ user_id, "user_signing", from_user_id
|
|
|
+ )
|
|
|
+ if key:
|
|
|
+ user_signing_keys[user_id] = key
|
|
|
+
|
|
|
+ return {
|
|
|
+ "master_keys": master_keys,
|
|
|
+ "self_signing_keys": self_signing_keys,
|
|
|
+ "user_signing_keys": user_signing_keys,
|
|
|
+ }
|
|
|
|
|
|
@trace
|
|
|
@defer.inlineCallbacks
|
|
@@ -441,6 +519,116 @@ class E2eKeysHandler(object):
|
|
|
log_kv({"message": "Inserting new one_time_keys.", "keys": new_keys})
|
|
|
yield self.store.add_e2e_one_time_keys(user_id, device_id, time_now, new_keys)
|
|
|
|
|
|
+ @defer.inlineCallbacks
|
|
|
+ def upload_signing_keys_for_user(self, user_id, keys):
|
|
|
+ """Upload signing keys for cross-signing
|
|
|
+
|
|
|
+ Args:
|
|
|
+ user_id (string): the user uploading the keys
|
|
|
+ keys (dict[string, dict]): the signing keys
|
|
|
+ """
|
|
|
+
|
|
|
+ # if a master key is uploaded, then check it. Otherwise, load the
|
|
|
+ # stored master key, to check signatures on other keys
|
|
|
+ if "master_key" in keys:
|
|
|
+ master_key = keys["master_key"]
|
|
|
+
|
|
|
+ _check_cross_signing_key(master_key, user_id, "master")
|
|
|
+ else:
|
|
|
+ master_key = yield self.store.get_e2e_cross_signing_key(user_id, "master")
|
|
|
+
|
|
|
+ # if there is no master key, then we can't do anything, because all the
|
|
|
+ # other cross-signing keys need to be signed by the master key
|
|
|
+ if not master_key:
|
|
|
+ raise SynapseError(400, "No master key available", Codes.MISSING_PARAM)
|
|
|
+
|
|
|
+ try:
|
|
|
+ master_key_id, master_verify_key = get_verify_key_from_cross_signing_key(
|
|
|
+ master_key
|
|
|
+ )
|
|
|
+ except ValueError:
|
|
|
+ if "master_key" in keys:
|
|
|
+ # the invalid key came from the request
|
|
|
+ raise SynapseError(400, "Invalid master key", Codes.INVALID_PARAM)
|
|
|
+ else:
|
|
|
+ # the invalid key came from the database
|
|
|
+ logger.error("Invalid master key found for user %s", user_id)
|
|
|
+ raise SynapseError(500, "Invalid master key")
|
|
|
+
|
|
|
+ # for the other cross-signing keys, make sure that they have valid
|
|
|
+ # signatures from the master key
|
|
|
+ if "self_signing_key" in keys:
|
|
|
+ self_signing_key = keys["self_signing_key"]
|
|
|
+
|
|
|
+ _check_cross_signing_key(
|
|
|
+ self_signing_key, user_id, "self_signing", master_verify_key
|
|
|
+ )
|
|
|
+
|
|
|
+ if "user_signing_key" in keys:
|
|
|
+ user_signing_key = keys["user_signing_key"]
|
|
|
+
|
|
|
+ _check_cross_signing_key(
|
|
|
+ user_signing_key, user_id, "user_signing", master_verify_key
|
|
|
+ )
|
|
|
+
|
|
|
+ # if everything checks out, then store the keys and send notifications
|
|
|
+ deviceids = []
|
|
|
+ if "master_key" in keys:
|
|
|
+ yield self.store.set_e2e_cross_signing_key(user_id, "master", master_key)
|
|
|
+ deviceids.append(master_verify_key.version)
|
|
|
+ if "self_signing_key" in keys:
|
|
|
+ yield self.store.set_e2e_cross_signing_key(
|
|
|
+ user_id, "self_signing", self_signing_key
|
|
|
+ )
|
|
|
+ try:
|
|
|
+ deviceids.append(
|
|
|
+ get_verify_key_from_cross_signing_key(self_signing_key)[1].version
|
|
|
+ )
|
|
|
+ except ValueError:
|
|
|
+ raise SynapseError(400, "Invalid self-signing key", Codes.INVALID_PARAM)
|
|
|
+ if "user_signing_key" in keys:
|
|
|
+ yield self.store.set_e2e_cross_signing_key(
|
|
|
+ user_id, "user_signing", user_signing_key
|
|
|
+ )
|
|
|
+ # the signature stream matches the semantics that we want for
|
|
|
+ # user-signing key updates: only the user themselves is notified of
|
|
|
+ # their own user-signing key updates
|
|
|
+ yield self.device_handler.notify_user_signature_update(user_id, [user_id])
|
|
|
+
|
|
|
+ # master key and self-signing key updates match the semantics of device
|
|
|
+ # list updates: all users who share an encrypted room are notified
|
|
|
+ if len(deviceids):
|
|
|
+ yield self.device_handler.notify_device_update(user_id, deviceids)
|
|
|
+
|
|
|
+ return {}
|
|
|
+
|
|
|
+
|
|
|
+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
|
|
|
+ checking, and ensures that it is signed, if a signature is required.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ key (dict): the key data to verify
|
|
|
+ user_id (str): the user whose key is being checked
|
|
|
+ key_type (str): the type of key that the key should be
|
|
|
+ signing_key (VerifyKey): (optional) the signing key that the key should
|
|
|
+ be signed with. If omitted, signatures will not be checked.
|
|
|
+ """
|
|
|
+ if (
|
|
|
+ key.get("user_id") != user_id
|
|
|
+ or key_type not in key.get("usage", [])
|
|
|
+ or len(key.get("keys", {})) != 1
|
|
|
+ ):
|
|
|
+ raise SynapseError(400, ("Invalid %s key" % (key_type,)), Codes.INVALID_PARAM)
|
|
|
+
|
|
|
+ if signing_key:
|
|
|
+ try:
|
|
|
+ verify_signed_json(key, user_id, signing_key)
|
|
|
+ except SignatureVerifyException:
|
|
|
+ raise SynapseError(
|
|
|
+ 400, ("Invalid signature on %s key" % key_type), Codes.INVALID_SIGNATURE
|
|
|
+ )
|
|
|
+
|
|
|
|
|
|
def _exception_to_failure(e):
|
|
|
if isinstance(e, CodeMessageException):
|