async def process_cross_signing_key_update( self, user_id: str, master_key: Optional[Dict[str, Any]], self_signing_key: Optional[Dict[str, Any]], ) -> list: """Process the given new master and self-signing key for the given remote user. Args: user_id: The ID of the user these keys are for. master_key: The dict of the cross-signing master key as returned by the remote server. self_signing_key: The dict of the cross-signing self-signing key as returned by the remote server. Return: The device IDs for the given keys. """ device_ids = [] if master_key: await self.store.set_e2e_cross_signing_key(user_id, "master", master_key) _, verify_key = get_verify_key_from_cross_signing_key(master_key) # 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 device_ids.append(verify_key.version) if self_signing_key: await self.store.set_e2e_cross_signing_key( user_id, "self_signing", self_signing_key ) _, verify_key = get_verify_key_from_cross_signing_key(self_signing_key) device_ids.append(verify_key.version) return device_ids
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. 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. This affects what signatures are fetched. Returns: dict, str, VerifyKey: the raw key data, the key ID, and the signedjson verify key Raises: NotFoundError: if the key is not found """ key = yield self.store.get_e2e_cross_signing_key( user_id, key_type, from_user_id) if key is None: logger.debug("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) return key, key_id, verify_key
def _handle_signing_key_updates(self, user_id): """Actually handle pending updates. Args: user_id (string): the user whose updates we are processing """ device_handler = self.e2e_keys_handler.device_handler with (yield self._remote_edu_linearizer.queue(user_id)): pending_updates = self._pending_updates.pop(user_id, []) if not pending_updates: # This can happen since we batch updates return device_ids = [] logger.info("pending updates: %r", pending_updates) for master_key, self_signing_key in pending_updates: if master_key: yield self.store.set_e2e_cross_signing_key( user_id, "master", master_key ) _, verify_key = get_verify_key_from_cross_signing_key(master_key) # 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 device_ids.append(verify_key.version) if self_signing_key: yield self.store.set_e2e_cross_signing_key( user_id, "self_signing", self_signing_key ) _, verify_key = get_verify_key_from_cross_signing_key( self_signing_key ) device_ids.append(verify_key.version) yield device_handler.notify_device_update(user_id, device_ids)
def get_device_updates_by_remote(self, destination, from_stream_id, limit): """Get a stream of device updates to send to the given remote server. Args: destination (str): The host the device updates are intended for from_stream_id (int): The minimum stream_id to filter updates by, exclusive limit (int): Maximum number of device updates to return Returns: Deferred[tuple[int, list[tuple[string,dict]]]]: current stream id (ie, the stream id of the last update included in the response), and the list of updates, where each update is a pair of EDU type and EDU contents """ now_stream_id = self._device_list_id_gen.get_current_token() has_changed = self._device_list_federation_stream_cache.has_entity_changed( destination, int(from_stream_id) ) if not has_changed: return now_stream_id, [] updates = yield self.db.runInteraction( "get_device_updates_by_remote", self._get_device_updates_by_remote_txn, destination, from_stream_id, now_stream_id, limit, ) # Return an empty list if there are no updates if not updates: return now_stream_id, [] # get the cross-signing keys of the users in the list, so that we can # determine which of the device changes were cross-signing keys users = {r[0] for r in updates} master_key_by_user = {} self_signing_key_by_user = {} for user in users: cross_signing_key = yield self.get_e2e_cross_signing_key(user, "master") if cross_signing_key: key_id, verify_key = get_verify_key_from_cross_signing_key( cross_signing_key ) # 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 master_key_by_user[user] = { "key_info": cross_signing_key, "device_id": verify_key.version, } cross_signing_key = yield self.get_e2e_cross_signing_key( user, "self_signing" ) if cross_signing_key: key_id, verify_key = get_verify_key_from_cross_signing_key( cross_signing_key ) self_signing_key_by_user[user] = { "key_info": cross_signing_key, "device_id": verify_key.version, } # Perform the equivalent of a GROUP BY # # Iterate through the updates list and copy non-duplicate # (user_id, device_id) entries into a map, with the value being # the max stream_id across each set of duplicate entries # # maps (user_id, device_id) -> (stream_id, opentracing_context) # # opentracing_context contains the opentracing metadata for the request # that created the poke # # The most recent request's opentracing_context is used as the # context which created the Edu. query_map = {} cross_signing_keys_by_user = {} for user_id, device_id, update_stream_id, update_context in updates: if ( user_id in master_key_by_user and device_id == master_key_by_user[user_id]["device_id"] ): result = cross_signing_keys_by_user.setdefault(user_id, {}) result["master_key"] = master_key_by_user[user_id]["key_info"] elif ( user_id in self_signing_key_by_user and device_id == self_signing_key_by_user[user_id]["device_id"] ): result = cross_signing_keys_by_user.setdefault(user_id, {}) result["self_signing_key"] = self_signing_key_by_user[user_id][ "key_info" ] else: key = (user_id, device_id) previous_update_stream_id, _ = query_map.get(key, (0, None)) if update_stream_id > previous_update_stream_id: query_map[key] = (update_stream_id, update_context) results = yield self._get_device_update_edus_by_remote( destination, from_stream_id, query_map ) # add the updated cross-signing keys to the results list for user_id, result in cross_signing_keys_by_user.items(): result["user_id"] = user_id # FIXME: switch to m.signing_key_update when MSC1756 is merged into the spec results.append(("org.matrix.signing_key_update", result)) return now_stream_id, results
def get_device_updates_by_remote(self, destination, from_stream_id, limit): """Get a stream of device updates to send to the given remote server. Args: destination (str): The host the device updates are intended for from_stream_id (int): The minimum stream_id to filter updates by, exclusive limit (int): Maximum number of device updates to return Returns: Deferred[tuple[int, list[tuple[string,dict]]]]: current stream id (ie, the stream id of the last update included in the response), and the list of updates, where each update is a pair of EDU type and EDU contents """ now_stream_id = self._device_list_id_gen.get_current_token() has_changed = self._device_list_federation_stream_cache.has_entity_changed( destination, int(from_stream_id) ) if not has_changed: return now_stream_id, [] # We retrieve n+1 devices from the list of outbound pokes where n is # our outbound device update limit. We then check if the very last # device has the same stream_id as the second-to-last device. If so, # then we ignore all devices with that stream_id and only send the # devices with a lower stream_id. # # If when culling the list we end up with no devices afterwards, we # consider the device update to be too large, and simply skip the # stream_id; the rationale being that such a large device list update # is likely an error. updates = yield self.db.runInteraction( "get_device_updates_by_remote", self._get_device_updates_by_remote_txn, destination, from_stream_id, now_stream_id, limit + 1, ) # Return an empty list if there are no updates if not updates: return now_stream_id, [] # get the cross-signing keys of the users in the list, so that we can # determine which of the device changes were cross-signing keys users = set(r[0] for r in updates) master_key_by_user = {} self_signing_key_by_user = {} for user in users: cross_signing_key = yield self.get_e2e_cross_signing_key(user, "master") if cross_signing_key: key_id, verify_key = get_verify_key_from_cross_signing_key( cross_signing_key ) # 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 master_key_by_user[user] = { "key_info": cross_signing_key, "device_id": verify_key.version, } cross_signing_key = yield self.get_e2e_cross_signing_key( user, "self_signing" ) if cross_signing_key: key_id, verify_key = get_verify_key_from_cross_signing_key( cross_signing_key ) self_signing_key_by_user[user] = { "key_info": cross_signing_key, "device_id": verify_key.version, } # if we have exceeded the limit, we need to exclude any results with the # same stream_id as the last row. if len(updates) > limit: stream_id_cutoff = updates[-1][2] now_stream_id = stream_id_cutoff - 1 else: stream_id_cutoff = None # Perform the equivalent of a GROUP BY # # Iterate through the updates list and copy non-duplicate # (user_id, device_id) entries into a map, with the value being # the max stream_id across each set of duplicate entries # # maps (user_id, device_id) -> (stream_id, opentracing_context) # as long as their stream_id does not match that of the last row # # opentracing_context contains the opentracing metadata for the request # that created the poke # # The most recent request's opentracing_context is used as the # context which created the Edu. query_map = {} cross_signing_keys_by_user = {} for user_id, device_id, update_stream_id, update_context in updates: if stream_id_cutoff is not None and update_stream_id >= stream_id_cutoff: # Stop processing updates break if ( user_id in master_key_by_user and device_id == master_key_by_user[user_id]["device_id"] ): result = cross_signing_keys_by_user.setdefault(user_id, {}) result["master_key"] = master_key_by_user[user_id]["key_info"] elif ( user_id in self_signing_key_by_user and device_id == self_signing_key_by_user[user_id]["device_id"] ): result = cross_signing_keys_by_user.setdefault(user_id, {}) result["self_signing_key"] = self_signing_key_by_user[user_id][ "key_info" ] else: key = (user_id, device_id) previous_update_stream_id, _ = query_map.get(key, (0, None)) if update_stream_id > previous_update_stream_id: query_map[key] = (update_stream_id, update_context) # If we didn't find any updates with a stream_id lower than the cutoff, it # means that there are more than limit updates all of which have the same # steam_id. # That should only happen if a client is spamming the server with new # devices, in which case E2E isn't going to work well anyway. We'll just # skip that stream_id and return an empty list, and continue with the next # stream_id next time. if not query_map and not cross_signing_keys_by_user: return stream_id_cutoff, [] results = yield self._get_device_update_edus_by_remote( destination, from_stream_id, query_map ) # add the updated cross-signing keys to the results list for user_id, result in iteritems(cross_signing_keys_by_user): result["user_id"] = user_id # FIXME: switch to m.signing_key_update when MSC1756 is merged into the spec results.append(("org.matrix.signing_key_update", result)) return now_stream_id, results
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 {}