diff --git a/synapse/handlers/device.py b/synapse/handlers/device.py
index 71a8f33da3..5f23ee4488 100644
--- a/synapse/handlers/device.py
+++ b/synapse/handlers/device.py
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2016 OpenMarket Ltd
+# Copyright 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.
@@ -439,6 +441,21 @@ class DeviceHandler(DeviceWorkerHandler):
log_kv({"message": "sent device update to host", "host": host})
@defer.inlineCallbacks
+ def notify_user_signature_update(self, from_user_id, user_ids):
+ """Notify a user that they have made new signatures of other users.
+
+ Args:
+ from_user_id (str): the user who made the signature
+ user_ids (list[str]): the users IDs that have new signatures
+ """
+
+ position = yield self.store.add_user_signature_change_to_streams(
+ from_user_id, user_ids
+ )
+
+ self.notifier.on_new_event("device_list_key", position, users=[from_user_id])
+
+ @defer.inlineCallbacks
def on_federation_query_user_devices(self, user_id):
stream_id, devices = yield self.store.get_devices_with_keys_by_user(user_id)
return {"user_id": user_id, "stream_id": stream_id, "devices": devices}
diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py
index 0a84d0e2b0..be675197f1 100644
--- a/synapse/handlers/e2e_keys.py
+++ b/synapse/handlers/e2e_keys.py
@@ -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):
diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py
index 50fc0fde2a..4b4c6c15f9 100644
--- a/synapse/handlers/federation.py
+++ b/synapse/handlers/federation.py
@@ -30,6 +30,7 @@ from unpaddedbase64 import decode_base64
from twisted.internet import defer
+from synapse import event_auth
from synapse.api.constants import EventTypes, Membership, RejectedReason
from synapse.api.errors import (
AuthError,
@@ -1763,7 +1764,7 @@ class FederationHandler(BaseHandler):
auth_for_e[(EventTypes.Create, "")] = create_event
try:
- self.auth.check(room_version, e, auth_events=auth_for_e)
+ event_auth.check(room_version, e, auth_events=auth_for_e)
except SynapseError as err:
# we may get SynapseErrors here as well as AuthErrors. For
# instance, there are a couple of (ancient) events in some
@@ -1919,7 +1920,7 @@ class FederationHandler(BaseHandler):
}
try:
- self.auth.check(room_version, event, auth_events=current_auth_events)
+ event_auth.check(room_version, event, auth_events=current_auth_events)
except AuthError as e:
logger.warn("Soft-failing %r because %s", event, e)
event.internal_metadata.soft_failed = True
@@ -2018,7 +2019,7 @@ class FederationHandler(BaseHandler):
)
try:
- self.auth.check(room_version, event, auth_events=auth_events)
+ event_auth.check(room_version, event, auth_events=auth_events)
except AuthError as e:
logger.warn("Failed auth resolution for %r because %s", event, e)
raise e
@@ -2181,103 +2182,10 @@ class FederationHandler(BaseHandler):
auth_events.update(new_state)
- different_auth = event_auth_events.difference(
- e.event_id for e in auth_events.values()
- )
-
yield self._update_context_for_auth_events(
event, context, auth_events, event_key
)
- if not different_auth:
- # we're done
- return
-
- logger.info(
- "auth_events still refers to events which are not in the calculated auth "
- "chain after state resolution: %s",
- different_auth,
- )
-
- # Only do auth resolution if we have something new to say.
- # We can't prove an auth failure.
- do_resolution = False
-
- for e_id in different_auth:
- if e_id in have_events:
- if have_events[e_id] == RejectedReason.NOT_ANCESTOR:
- do_resolution = True
- break
-
- if not do_resolution:
- logger.info(
- "Skipping auth resolution due to lack of provable rejection reasons"
- )
- return
-
- logger.info("Doing auth resolution")
-
- prev_state_ids = yield context.get_prev_state_ids(self.store)
-
- # 1. Get what we think is the auth chain.
- auth_ids = yield self.auth.compute_auth_events(event, prev_state_ids)
- local_auth_chain = yield self.store.get_auth_chain(auth_ids, include_given=True)
-
- try:
- # 2. Get remote difference.
- try:
- result = yield self.federation_client.query_auth(
- origin, event.room_id, event.event_id, local_auth_chain
- )
- except RequestSendFailed as e:
- # The other side isn't around or doesn't implement the
- # endpoint, so lets just bail out.
- logger.info("Failed to query auth from remote: %s", e)
- return
-
- seen_remotes = yield self.store.have_seen_events(
- [e.event_id for e in result["auth_chain"]]
- )
-
- # 3. Process any remote auth chain events we haven't seen.
- for ev in result["auth_chain"]:
- if ev.event_id in seen_remotes:
- continue
-
- if ev.event_id == event.event_id:
- continue
-
- try:
- auth_ids = ev.auth_event_ids()
- auth = {
- (e.type, e.state_key): e
- for e in result["auth_chain"]
- if e.event_id in auth_ids or event.type == EventTypes.Create
- }
- ev.internal_metadata.outlier = True
-
- logger.debug(
- "do_auth %s different_auth: %s", event.event_id, e.event_id
- )
-
- yield self._handle_new_event(origin, ev, auth_events=auth)
-
- if ev.event_id in event_auth_events:
- auth_events[(ev.type, ev.state_key)] = ev
- except AuthError:
- pass
-
- except Exception:
- # FIXME:
- logger.exception("Failed to query auth chain")
-
- # 4. Look at rejects and their proofs.
- # TODO.
-
- yield self._update_context_for_auth_events(
- event, context, auth_events, event_key
- )
-
@defer.inlineCallbacks
def _update_context_for_auth_events(self, event, context, auth_events, event_key):
"""Update the state_ids in an event context after auth event resolution,
@@ -2444,15 +2352,6 @@ class FederationHandler(BaseHandler):
reason_map[e.event_id] = reason
- if reason == RejectedReason.AUTH_ERROR:
- pass
- elif reason == RejectedReason.REPLACED:
- # TODO: Get proof
- pass
- elif reason == RejectedReason.NOT_ANCESTOR:
- # TODO: Get proof.
- pass
-
logger.debug("construct_auth_difference returning")
return {
diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py
index 19bca6717f..d99160e9d7 100644
--- a/synapse/handlers/sync.py
+++ b/synapse/handlers/sync.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2015, 2016 OpenMarket Ltd
-# Copyright 2018 New Vector Ltd
+# Copyright 2018, 2019 New Vector Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -1124,6 +1124,11 @@ class SyncHandler(object):
# weren't in the previous sync *or* they left and rejoined.
users_that_have_changed.update(newly_joined_or_invited_users)
+ user_signatures_changed = yield self.store.get_users_whose_signatures_changed(
+ user_id, since_token.device_list_key
+ )
+ users_that_have_changed.update(user_signatures_changed)
+
# Now find users that we no longer track
for room_id in newly_left_rooms:
left_users = yield self.state.get_current_users_in_room(room_id)
|