summary refs log tree commit diff
diff options
context:
space:
mode:
authorHubert Chathi <hubert@uhoreg.ca>2019-05-22 16:41:24 -0400
committerHubert Chathi <hubert@uhoreg.ca>2019-09-04 20:02:56 -0400
commit4bb454478470c6b707d33292113ac3a23010db8b (patch)
tree7fbfb83aaf491a6b0f5f2ead8e56e3d4b491a072
parentadd user signature stream change cache to slaved device store (diff)
downloadsynapse-4bb454478470c6b707d33292113ac3a23010db8b.tar.xz
implement device signature uploading/fetching
-rw-r--r--synapse/handlers/e2e_keys.py250
-rw-r--r--synapse/rest/client/v2_alpha/keys.py50
-rw-r--r--synapse/storage/end_to_end_keys.py38
3 files changed, 338 insertions, 0 deletions
diff --git a/synapse/handlers/e2e_keys.py b/synapse/handlers/e2e_keys.py
index 997ad66f8f..9747b517ff 100644
--- a/synapse/handlers/e2e_keys.py
+++ b/synapse/handlers/e2e_keys.py
@@ -608,6 +608,194 @@ class E2eKeysHandler(object):
 
         return {}
 
+    @defer.inlineCallbacks
+    def upload_signatures_for_device_keys(self, user_id, signatures):
+        """Upload device signatures for cross-signing
+
+        Args:
+            user_id (string): the user uploading the signatures
+            signatures (dict[string, dict[string, dict]]): map of users to
+                devices to signed keys
+        """
+        failures = {}
+
+        signature_list = []   # signatures to be stored
+        self_device_ids = []  # what devices have been updated, for notifying
+
+        # split between checking signatures for own user and signatures for
+        # other users, since we verify them with different keys
+        if user_id in signatures:
+            self_signatures = signatures[user_id]
+            del signatures[user_id]
+            self_device_ids = list(self_signatures.keys())
+            try:
+                # get our self-signing key to verify the signatures
+                self_signing_key = yield self.store.get_e2e_cross_signing_key(
+                    user_id, "self_signing"
+                )
+                if self_signing_key is None:
+                    raise SynapseError(
+                        404,
+                        "No self-signing key found",
+                        Codes.NOT_FOUND
+                    )
+
+                self_signing_key_id, self_signing_verify_key \
+                    = get_verify_key_from_cross_signing_key(self_signing_key)
+
+                # fetch our stored devices so that we can compare with what was sent
+                user_devices = []
+                for device in self_signatures.keys():
+                    user_devices.append((user_id, device))
+                devices = yield self.store.get_e2e_device_keys(user_devices)
+
+                if user_id not in devices:
+                    raise SynapseError(
+                        404,
+                        "No device key found",
+                        Codes.NOT_FOUND
+                    )
+
+                devices = devices[user_id]
+                for device_id, device in self_signatures.items():
+                    try:
+                        if ("signatures" not in device or
+                                user_id not in device["signatures"] or
+                                self_signing_key_id not in device["signatures"][user_id]):
+                            # no signature was sent
+                            raise SynapseError(
+                                400,
+                                "Invalid signature",
+                                Codes.INVALID_SIGNATURE
+                            )
+
+                        stored_device = devices[device_id]["keys"]
+                        if self_signing_key_id in stored_device.get("signatures", {}) \
+                                                               .get(user_id, {}):
+                            # we already have a signature on this device, so we
+                            # can skip it, since it should be exactly the same
+                            continue
+
+                        _check_device_signature(
+                            user_id, self_signing_verify_key, device, stored_device
+                        )
+
+                        signature = device["signatures"][user_id][self_signing_key_id]
+                        signature_list.append(
+                            (self_signing_key_id, user_id, device_id, signature)
+                        )
+                    except SynapseError as e:
+                        failures.setdefault(user_id, {})[device_id] \
+                            = _exception_to_failure(e)
+            except SynapseError as e:
+                failures[user_id] = {
+                    device: _exception_to_failure(e)
+                    for device in self_signatures.keys()
+                }
+
+        signed_users = []  # what user have been signed, for notifying
+        if len(signatures):
+            # if signatures isn't empty, then we have signatures for other
+            # users.  These signatures will be signed by the user signing key
+
+            # get our user-signing key to verify the signatures
+            user_signing_key = yield self.store.get_e2e_cross_signing_key(
+                user_id, "user_signing"
+            )
+            if user_signing_key is None:
+                for user, devicemap in signatures.items():
+                    failures[user] = {
+                        device: _exception_to_failure(SynapseError(
+                            404,
+                            "No user-signing key found",
+                            Codes.NOT_FOUND
+                        ))
+                        for device in devicemap.keys()
+                    }
+            else:
+                user_signing_key_id, user_signing_verify_key \
+                    = get_verify_key_from_cross_signing_key(user_signing_key)
+
+                for user, devicemap in signatures.items():
+                    device_id = None
+                    try:
+                        # get the user's master key, to make sure it matches
+                        # what was sent
+                        stored_key = yield self.store.get_e2e_cross_signing_key(
+                            user, "master", user_id
+                        )
+                        if stored_key is None:
+                            logger.error(
+                                "upload signature: no user key found for %s", user
+                            )
+                            raise SynapseError(
+                                404,
+                                "User's master key not found",
+                                Codes.NOT_FOUND
+                            )
+
+                        # make sure that the user's master key is the one that
+                        # was signed (and no others)
+                        device_id = get_verify_key_from_cross_signing_key(stored_key)[0] \
+                            .split(":", 1)[1]
+                        if device_id not in devicemap:
+                            logger.error(
+                                "upload signature: wrong device: %s vs %s",
+                                device, devicemap
+                            )
+                            raise SynapseError(
+                                404,
+                                "Unknown device",
+                                Codes.NOT_FOUND
+                            )
+                        if len(devicemap) > 1:
+                            logger.error("upload signature: too many devices specified")
+                            failures[user] = {
+                                device: _exception_to_failure(SynapseError(
+                                    404,
+                                    "Unknown device",
+                                    Codes.NOT_FOUND
+                                ))
+                                for device in devicemap.keys()
+                            }
+
+                        key = devicemap[device_id]
+
+                        if user_signing_key_id in stored_key.get("signatures", {}) \
+                                                            .get(user_id, {}):
+                            # we already have the signature, so we can skip it
+                            continue
+
+                        _check_device_signature(
+                            user_id, user_signing_verify_key, key, stored_key
+                        )
+
+                        signature = key["signatures"][user_id][user_signing_key_id]
+
+                        signed_users.append(user)
+                        signature_list.append(
+                            (user_signing_key_id, user, device_id, signature)
+                        )
+                    except SynapseError as e:
+                        if device_id is None:
+                            failures[user] = {
+                                device_id: _exception_to_failure(e)
+                                for device_id in devicemap.keys()
+                            }
+                        else:
+                            failures.setdefault(user, {})[device_id] \
+                                = _exception_to_failure(e)
+
+        # store the signature, and send the appropriate notifications for sync
+        logger.debug("upload signature failures: %r", failures)
+        yield self.store.store_e2e_device_signatures(user_id, signature_list)
+
+        if len(self_device_ids):
+            yield self.device_handler.notify_device_update(user_id, self_device_ids)
+        if len(signed_users):
+            yield self.device_handler.notify_user_signature_update(user_id, signed_users)
+
+        defer.returnValue({"failures": failures})
 
 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
@@ -636,6 +824,68 @@ def _check_cross_signing_key(key, user_id, key_type, signing_key=None):
             )
 
 
+def _check_device_signature(user_id, verify_key, signed_device, stored_device):
+    """Check that a device signature is correct and matches the copy of the device
+    that we have.  Throws an exception if an error is detected.
+
+    Args:
+        user_id (str): the user ID whose signature is being checked
+        verify_key (VerifyKey): the key to verify the device with
+        signed_device (dict): the signed device data
+        stored_device (dict): our previous copy of the device
+    """
+
+    key_id = "%s:%s" % (verify_key.alg, verify_key.version)
+
+    # make sure the device is signed
+    if ("signatures" not in signed_device or user_id not in signed_device["signatures"]
+            or key_id not in signed_device["signatures"][user_id]):
+        logger.error("upload signature: user not found in signatures")
+        raise SynapseError(
+            400,
+            "Invalid signature",
+            Codes.INVALID_SIGNATURE
+        )
+
+    signature = signed_device["signatures"][user_id][key_id]
+
+    # make sure that the device submitted matches what we have stored
+    del signed_device["signatures"]
+    if "unsigned" in signed_device:
+        del signed_device["unsigned"]
+    if "signatures" in stored_device:
+        del stored_device["signatures"]
+    if "unsigned" in stored_device:
+        del stored_device["unsigned"]
+    if signed_device != stored_device:
+        logger.error(
+            "upload signatures: key does not match %s vs %s",
+            signed_device, stored_device
+        )
+        raise SynapseError(
+            400,
+            "Key does not match",
+            "M_MISMATCHED_KEY"
+        )
+
+    # check the signature
+    signed_device["signatures"] = {
+        user_id: {
+            key_id: signature
+        }
+    }
+
+    try:
+        verify_signed_json(signed_device, user_id, verify_key)
+    except SignatureVerifyException:
+        logger.error("invalid signature on key")
+        raise SynapseError(
+            400,
+            "Invalid signature",
+            Codes.INVALID_SIGNATURE
+        )
+
+
 def _exception_to_failure(e):
     if isinstance(e, CodeMessageException):
         return {"status": e.code, "message": str(e)}
diff --git a/synapse/rest/client/v2_alpha/keys.py b/synapse/rest/client/v2_alpha/keys.py
index 151a70d449..5c288d48b7 100644
--- a/synapse/rest/client/v2_alpha/keys.py
+++ b/synapse/rest/client/v2_alpha/keys.py
@@ -277,9 +277,59 @@ class SigningKeyUploadServlet(RestServlet):
         return (200, result)
 
 
+class SignaturesUploadServlet(RestServlet):
+    """
+    POST /keys/signatures/upload HTTP/1.1
+    Content-Type: application/json
+
+    {
+      "@alice:example.com": {
+        "<device_id>": {
+          "user_id": "<user_id>",
+          "device_id": "<device_id>",
+          "algorithms": [
+            "m.olm.curve25519-aes-sha256",
+            "m.megolm.v1.aes-sha"
+          ],
+          "keys": {
+            "<algorithm>:<device_id>": "<key_base64>",
+          },
+          "signatures": {
+            "<signing_user_id>": {
+              "<algorithm>:<signing_key_base64>": "<signature_base64>>"
+            }
+          }
+        }
+      }
+    }
+    """
+    PATTERNS = client_v2_patterns("/keys/signatures/upload$")
+
+    def __init__(self, hs):
+        """
+        Args:
+            hs (synapse.server.HomeServer): server
+        """
+        super(SignaturesUploadServlet, self).__init__()
+        self.auth = hs.get_auth()
+        self.e2e_keys_handler = hs.get_e2e_keys_handler()
+
+    @defer.inlineCallbacks
+    def on_POST(self, request):
+        requester = yield self.auth.get_user_by_req(request, allow_guest=True)
+        user_id = requester.user.to_string()
+        body = parse_json_object_from_request(request)
+
+        result = yield self.e2e_keys_handler.upload_signatures_for_device_keys(
+            user_id, body
+        )
+        defer.returnValue((200, result))
+
+
 def register_servlets(hs, http_server):
     KeyUploadServlet(hs).register(http_server)
     KeyQueryServlet(hs).register(http_server)
     KeyChangesServlet(hs).register(http_server)
     OneTimeKeyServlet(hs).register(http_server)
     SigningKeyUploadServlet(hs).register(http_server)
+    SignaturesUploadServlet(hs).register(http_server)
diff --git a/synapse/storage/end_to_end_keys.py b/synapse/storage/end_to_end_keys.py
index 8ce5dd8bf9..fe786f3093 100644
--- a/synapse/storage/end_to_end_keys.py
+++ b/synapse/storage/end_to_end_keys.py
@@ -59,6 +59,12 @@ class EndToEndKeyWorkerStore(SQLBaseStore):
         for user_id, device_keys in iteritems(results):
             for device_id, device_info in iteritems(device_keys):
                 device_info["keys"] = db_to_json(device_info.pop("key_json"))
+                # add cross-signing signatures to the keys
+                if "signatures" in device_info:
+                    for sig_user_id, sigs in device_info["signatures"].items():
+                        device_info["keys"].setdefault("signatures", {}) \
+                                           .setdefault(sig_user_id, {}) \
+                                           .update(sigs)
 
         return results
 
@@ -71,6 +77,8 @@ class EndToEndKeyWorkerStore(SQLBaseStore):
 
         query_clauses = []
         query_params = []
+        signature_query_clauses = []
+        signature_query_params = []
 
         if include_all_devices is False:
             include_deleted_devices = False
@@ -81,12 +89,20 @@ class EndToEndKeyWorkerStore(SQLBaseStore):
         for (user_id, device_id) in query_list:
             query_clause = "user_id = ?"
             query_params.append(user_id)
+            signature_query_clause = "target_user_id = ?"
+            signature_query_params.append(user_id)
 
             if device_id is not None:
                 query_clause += " AND device_id = ?"
                 query_params.append(device_id)
+                signature_query_clause += " AND target_device_id = ?"
+                signature_query_params.append(device_id)
+
+            signature_query_clause += " AND user_id = ?"
+            signature_query_params.append(user_id)
 
             query_clauses.append(query_clause)
+            signature_query_clauses.append(signature_query_clause)
 
         sql = (
             "SELECT user_id, device_id, "
@@ -113,6 +129,28 @@ class EndToEndKeyWorkerStore(SQLBaseStore):
             for user_id, device_id in deleted_devices:
                 result.setdefault(user_id, {})[device_id] = None
 
+        # get signatures on the device
+        signature_sql = (
+            "SELECT * "
+            "  FROM e2e_device_signatures "
+            " WHERE %s"
+        ) % (
+            " OR ".join("(" + q + ")" for q in signature_query_clauses)
+        )
+
+        txn.execute(signature_sql, signature_query_params)
+        rows = self.cursor_to_dict(txn)
+
+        for row in rows:
+            target_user_id = row["target_user_id"]
+            target_device_id = row["target_device_id"]
+            if target_user_id in result \
+               and target_device_id in result[target_user_id]:
+                result[target_user_id][target_device_id] \
+                    .setdefault("signatures", {}) \
+                    .setdefault(row["user_id"], {})[row["key_id"]] \
+                    = row["signature"]
+
         log_kv(result)
         return result