implement uploading/downloading of cross-signing keys
This commit is contained in:
1
changelog.d/4970.feature
Normal file
1
changelog.d/4970.feature
Normal file
@@ -0,0 +1 @@
|
||||
Add support for cross-signing.
|
||||
@@ -61,6 +61,7 @@ class Codes(object):
|
||||
INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION"
|
||||
WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
|
||||
EXPIRED_ACCOUNT = "ORG_MATRIX_EXPIRED_ACCOUNT"
|
||||
INVALID_SIGNATURE = "M_INVALID_SIGNATURE"
|
||||
|
||||
|
||||
class CodeMessageException(RuntimeError):
|
||||
|
||||
@@ -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.
|
||||
@@ -419,6 +421,23 @@ class DeviceHandler(DeviceWorkerHandler):
|
||||
for host in hosts:
|
||||
self.federation_sender.send_device_messages(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)
|
||||
|
||||
@@ -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,11 +20,23 @@ 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, FederationDeniedError, SynapseError
|
||||
from synapse.types import UserID, get_domain_from_id
|
||||
from synapse.api.errors import (
|
||||
CodeMessageException,
|
||||
Codes,
|
||||
FederationDeniedError,
|
||||
SynapseError,
|
||||
)
|
||||
from synapse.types import (
|
||||
UserID,
|
||||
get_domain_from_id,
|
||||
get_verify_key_from_cross_signing_key,
|
||||
)
|
||||
from synapse.util.async_helpers import Linearizer
|
||||
from synapse.util.caches.expiringcache import ExpiringCache
|
||||
from synapse.util.logcontext import make_deferred_yieldable, run_in_background
|
||||
from synapse.util.retryutils import NotRetryingDestination
|
||||
|
||||
@@ -46,7 +59,7 @@ class E2eKeysHandler(object):
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def query_devices(self, query_body, timeout):
|
||||
def query_devices(self, query_body, timeout, from_user_id=None):
|
||||
""" Handle a device key query from a client
|
||||
|
||||
{
|
||||
@@ -64,6 +77,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", {})
|
||||
|
||||
@@ -120,6 +138,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.query_cross_signing_keys(
|
||||
device_keys_query, from_user_id
|
||||
)
|
||||
|
||||
# Now fetch any devices that we don't have in our cache
|
||||
@defer.inlineCallbacks
|
||||
def do_remote_query(destination):
|
||||
@@ -135,6 +158,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"][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"][user_id] = key
|
||||
|
||||
except Exception as e:
|
||||
failures[destination] = _exception_to_failure(e)
|
||||
|
||||
@@ -143,9 +174,60 @@ class E2eKeysHandler(object):
|
||||
for destination in remote_queries_not_in_cache
|
||||
], consumeErrors=True))
|
||||
|
||||
defer.returnValue({
|
||||
ret = {
|
||||
"device_keys": results, "failures": failures,
|
||||
})
|
||||
}
|
||||
|
||||
for key, value in iteritems(cross_signing_keys):
|
||||
ret[key + "_keys"] = value
|
||||
|
||||
defer.returnValue(ret)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def query_cross_signing_keys(self, query, from_user_id=None):
|
||||
"""Get cross-signing keys for users
|
||||
|
||||
Args:
|
||||
query (dict[string, *]): map from user_id. This function only looks
|
||||
at the dict's keys, and the values are ignored, so the query
|
||||
format used for query_devices can be used.
|
||||
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: (resolves to dict[string, dict[string, dict]]): map from
|
||||
(master|self_signing) -> map from user_id -> master key
|
||||
"""
|
||||
master_keys = {}
|
||||
self_signing_keys = {}
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_cross_signing_key(user_id):
|
||||
try:
|
||||
key = yield self.store.get_e2e_cross_signing_key(
|
||||
user_id, "master", from_user_id
|
||||
)
|
||||
if key:
|
||||
master_keys[user_id] = key
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
key = yield self.store.get_e2e_cross_signing_key(
|
||||
user_id, "self_signing", from_user_id
|
||||
)
|
||||
if key:
|
||||
self_signing_keys[user_id] = key
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
yield make_deferred_yieldable(defer.gatherResults([
|
||||
run_in_background(get_cross_signing_key, user_id)
|
||||
for user_id in query.keys()
|
||||
]))
|
||||
|
||||
defer.returnValue({"master": master_keys, "self_signing": self_signing_keys})
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def query_local_devices(self, query):
|
||||
@@ -337,6 +419,113 @@ class E2eKeysHandler(object):
|
||||
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
|
||||
)
|
||||
|
||||
master_key_id, master_verify_key = get_verify_key_from_cross_signing_key(
|
||||
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
|
||||
)
|
||||
deviceids.append(
|
||||
get_verify_key_from_cross_signing_key(self_signing_key)[1].version
|
||||
)
|
||||
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.defice_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)
|
||||
|
||||
defer.returnValue({})
|
||||
|
||||
|
||||
|
||||
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 "user_id" not in key or key["user_id"] != user_id \
|
||||
or "usage" not in key or key_type not in key["usage"]:
|
||||
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 or %s key" % key_type),
|
||||
Codes.INVALID_SIGNATURE
|
||||
)
|
||||
|
||||
def _exception_to_failure(e):
|
||||
if isinstance(e, CodeMessageException):
|
||||
|
||||
@@ -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.
|
||||
@@ -1061,6 +1061,11 @@ class SyncHandler(object):
|
||||
# weren't in the previous sync *or* they left and rejoined.
|
||||
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
|
||||
)
|
||||
changed.update(user_signatures_changed)
|
||||
|
||||
if not changed and not newly_left_users:
|
||||
defer.returnValue(DeviceLists(
|
||||
changed=[],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2015, 2016 OpenMarket Ltd
|
||||
# Copyright 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.
|
||||
@@ -26,7 +27,7 @@ from synapse.http.servlet import (
|
||||
)
|
||||
from synapse.types import StreamToken
|
||||
|
||||
from ._base import client_v2_patterns
|
||||
from ._base import client_v2_patterns, interactive_auth_handler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -143,10 +144,11 @@ class KeyQueryServlet(RestServlet):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request):
|
||||
yield self.auth.get_user_by_req(request, allow_guest=True)
|
||||
requester = yield self.auth.get_user_by_req(request, allow_guest=True)
|
||||
user_id = requester.user.to_string()
|
||||
timeout = parse_integer(request, "timeout", 10 * 1000)
|
||||
body = parse_json_object_from_request(request)
|
||||
result = yield self.e2e_keys_handler.query_devices(body, timeout)
|
||||
result = yield self.e2e_keys_handler.query_devices(body, timeout, user_id)
|
||||
defer.returnValue((200, result))
|
||||
|
||||
|
||||
@@ -228,8 +230,47 @@ class OneTimeKeyServlet(RestServlet):
|
||||
defer.returnValue((200, result))
|
||||
|
||||
|
||||
class SigningKeyUploadServlet(RestServlet):
|
||||
"""
|
||||
POST /keys/device_signing/upload HTTP/1.1
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
}
|
||||
"""
|
||||
PATTERNS = client_v2_patterns("/keys/device_signing/upload$")
|
||||
|
||||
def __init__(self, hs):
|
||||
"""
|
||||
Args:
|
||||
hs (synapse.server.HomeServer): server
|
||||
"""
|
||||
super(SigningKeyUploadServlet, self).__init__()
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.e2e_keys_handler = hs.get_e2e_keys_handler()
|
||||
self.auth_handler = hs.get_auth_handler()
|
||||
|
||||
@interactive_auth_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)
|
||||
|
||||
yield self.auth_handler.validate_user_via_ui_auth(
|
||||
requester, body, self.hs.get_ip_from_request(request),
|
||||
)
|
||||
|
||||
result = yield self.e2e_keys_handler.upload_signing_keys_for_user(
|
||||
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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-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.
|
||||
@@ -205,6 +205,9 @@ class DataStore(
|
||||
self._device_list_stream_cache = StreamChangeCache(
|
||||
"DeviceListStreamChangeCache", device_list_max
|
||||
)
|
||||
self._user_signature_stream_cache = StreamChangeCache(
|
||||
"UserSignatureStreamChangeCache", device_list_max,
|
||||
)
|
||||
self._device_list_federation_stream_cache = StreamChangeCache(
|
||||
"DeviceListFederationStreamChangeCache", device_list_max
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
@@ -20,10 +22,11 @@ from canonicaljson import json
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.errors import StoreError
|
||||
from synapse.api.errors import Codes, StoreError
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.storage._base import Cache, SQLBaseStore, db_to_json
|
||||
from synapse.storage.background_updates import BackgroundUpdateStore
|
||||
from synapse.types import get_verify_key_from_cross_signing_key
|
||||
from synapse.util.caches.descriptors import cached, cachedInlineCallbacks, cachedList
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -47,7 +50,7 @@ class DeviceWorkerStore(SQLBaseStore):
|
||||
"""
|
||||
return self._simple_select_one(
|
||||
table="devices",
|
||||
keyvalues={"user_id": user_id, "device_id": device_id},
|
||||
keyvalues={"user_id": user_id, "device_id": device_id, "hidden": False},
|
||||
retcols=("user_id", "device_id", "display_name"),
|
||||
desc="get_device",
|
||||
)
|
||||
@@ -65,7 +68,7 @@ class DeviceWorkerStore(SQLBaseStore):
|
||||
"""
|
||||
devices = yield self._simple_select_list(
|
||||
table="devices",
|
||||
keyvalues={"user_id": user_id},
|
||||
keyvalues={"user_id": user_id, "hidden": False},
|
||||
retcols=("user_id", "device_id", "display_name"),
|
||||
desc="get_devices_by_user",
|
||||
)
|
||||
@@ -205,6 +208,33 @@ class DeviceWorkerStore(SQLBaseStore):
|
||||
"""
|
||||
txn.execute(sql, (destination, stream_id))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def add_user_signature_change_to_streams(self, from_user_id, user_ids):
|
||||
"""Persist that a user has made new signatures
|
||||
"""
|
||||
|
||||
with self._device_list_id_gen.get_next() as stream_id:
|
||||
yield self.runInteraction(
|
||||
"add_user_sig_change_to_streams", self._add_user_signature_change_txn,
|
||||
from_user_id, user_ids, stream_id,
|
||||
)
|
||||
defer.returnValue(stream_id)
|
||||
|
||||
def _add_user_signature_change_txn(self, txn, from_user_id, user_ids, stream_id):
|
||||
txn.call_after(
|
||||
self._user_signature_stream_cache.entity_has_changed,
|
||||
from_user_id, stream_id,
|
||||
)
|
||||
self._simple_insert_txn(
|
||||
txn,
|
||||
"user_signature_stream",
|
||||
values={
|
||||
"stream_id": stream_id,
|
||||
"from_user_id": from_user_id,
|
||||
"user_ids": json.dumps(user_ids),
|
||||
},
|
||||
)
|
||||
|
||||
def get_device_stream_token(self):
|
||||
return self._device_list_id_gen.get_current_token()
|
||||
|
||||
@@ -318,6 +348,25 @@ class DeviceWorkerStore(SQLBaseStore):
|
||||
)
|
||||
defer.returnValue(set(row[0] for row in rows))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_users_whose_signatures_changed(self, user_id, from_key):
|
||||
"""Get set of users who have new cross-signing signatures have changed since
|
||||
`from_key`.
|
||||
|
||||
"""
|
||||
from_key = int(from_key)
|
||||
if self._user_signature_stream_cache.has_entity_changed(user_id, from_key):
|
||||
sql = """
|
||||
SELECT DISTINCT user_ids FROM user_signature_stream
|
||||
WHERE from_user_id = ? AND stream_id > ?
|
||||
"""
|
||||
rows = yield self._execute(
|
||||
"get_users_whose_signatures_changed", None, sql, user_id, from_key
|
||||
)
|
||||
defer.returnValue(set(user for row in rows for user in json.loads(row[0])))
|
||||
else:
|
||||
defer.returnValue(set())
|
||||
|
||||
def get_all_device_list_changes_for_remotes(self, from_key, to_key):
|
||||
"""Return a list of `(stream_id, user_id, destination)` which is the
|
||||
combined list of changes to devices, and which destinations need to be
|
||||
@@ -436,12 +485,30 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore):
|
||||
"user_id": user_id,
|
||||
"device_id": device_id,
|
||||
"display_name": initial_device_display_name,
|
||||
"hidden": False
|
||||
},
|
||||
desc="store_device",
|
||||
or_ignore=True,
|
||||
)
|
||||
if not inserted:
|
||||
# if the device already exists, check if it's a real device, or
|
||||
# if the device ID is reserved by something else
|
||||
hidden = yield self._simple_select_one_onecol(
|
||||
"devices",
|
||||
keyvalues={
|
||||
"user_id": user_id,
|
||||
"device_id": device_id
|
||||
},
|
||||
retcol="hidden"
|
||||
)
|
||||
if hidden:
|
||||
raise StoreError(
|
||||
400, "The device ID is reserved", Codes.FORBIDDEN
|
||||
)
|
||||
self.device_id_exists_cache.prefill(key, True)
|
||||
defer.returnValue(inserted)
|
||||
except StoreError as e:
|
||||
raise e
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"store_device with device_id=%s(%r) user_id=%s(%r)"
|
||||
@@ -468,7 +535,7 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore):
|
||||
"""
|
||||
yield self._simple_delete_one(
|
||||
table="devices",
|
||||
keyvalues={"user_id": user_id, "device_id": device_id},
|
||||
keyvalues={"user_id": user_id, "device_id": device_id, "hidden": False},
|
||||
desc="delete_device",
|
||||
)
|
||||
|
||||
@@ -488,7 +555,7 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore):
|
||||
table="devices",
|
||||
column="device_id",
|
||||
iterable=device_ids,
|
||||
keyvalues={"user_id": user_id},
|
||||
keyvalues={"user_id": user_id, "hidden": False},
|
||||
desc="delete_devices",
|
||||
)
|
||||
for device_id in device_ids:
|
||||
@@ -514,7 +581,7 @@ class DeviceStore(DeviceWorkerStore, BackgroundUpdateStore):
|
||||
return defer.succeed(None)
|
||||
return self._simple_update_one(
|
||||
table="devices",
|
||||
keyvalues={"user_id": user_id, "device_id": device_id},
|
||||
keyvalues={"user_id": user_id, "device_id": device_id, "hidden": False},
|
||||
updatevalues=updates,
|
||||
desc="update_device",
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2015, 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.
|
||||
@@ -12,9 +14,11 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import time
|
||||
|
||||
from six import iteritems
|
||||
|
||||
from canonicaljson import encode_canonical_json
|
||||
from canonicaljson import encode_canonical_json, json
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
@@ -85,7 +89,7 @@ class EndToEndKeyWorkerStore(SQLBaseStore):
|
||||
" k.key_json"
|
||||
" FROM devices d"
|
||||
" %s JOIN e2e_device_keys_json k USING (user_id, device_id)"
|
||||
" WHERE %s"
|
||||
" WHERE (%s) AND d.hidden = 0"
|
||||
) % (
|
||||
"LEFT" if include_all_devices else "INNER",
|
||||
" OR ".join("(" + q + ")" for q in query_clauses),
|
||||
@@ -281,3 +285,145 @@ class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore):
|
||||
return self.runInteraction(
|
||||
"delete_e2e_keys_by_device", delete_e2e_keys_by_device_txn
|
||||
)
|
||||
|
||||
def _set_e2e_device_signing_key_txn(self, txn, user_id, key_type, key):
|
||||
"""Set a user's cross-signing key.
|
||||
|
||||
Args:
|
||||
txn (twisted.enterprise.adbapi.Connection): db connection
|
||||
user_id (str): the user to set the signing key for
|
||||
key_type (str): the type of key that is being set: either 'master'
|
||||
for a master key, 'self_signing' for a self-signing key, or
|
||||
'user_signing' for a user-signing key
|
||||
key (dict): the key data
|
||||
"""
|
||||
# the cross-signing keys need to occupy the same namespace as devices,
|
||||
# since signatures are identified by device ID. So add an entry to the
|
||||
# device table to make sure that we don't have a collision with device
|
||||
# IDs
|
||||
for v in key["keys"].values():
|
||||
pubkey = v
|
||||
break
|
||||
self._simple_insert(
|
||||
"devices",
|
||||
values={
|
||||
"user_id": user_id,
|
||||
"device_id": pubkey,
|
||||
"display_name": key_type + " signing key",
|
||||
"hidden": True
|
||||
},
|
||||
desc="store_master_key_device"
|
||||
)
|
||||
|
||||
# and finally, store the key itself
|
||||
self._simple_insert(
|
||||
"e2e_device_signing_keys",
|
||||
values={
|
||||
"user_id": user_id,
|
||||
"keytype": key_type,
|
||||
"keydata": json.dumps(key),
|
||||
"ts": time.time() * 1000
|
||||
},
|
||||
desc="store_master_key"
|
||||
)
|
||||
|
||||
def set_e2e_cross_signing_key(self, user_id, key_type, key):
|
||||
"""Set a user's cross-signing key.
|
||||
|
||||
Args:
|
||||
user_id (str): the user to set the user-signing key for
|
||||
key_type (str): the type of cross-signing key to set
|
||||
key (dict): the key data
|
||||
"""
|
||||
return self.runInteraction(
|
||||
"add_e2e_device_signing_key",
|
||||
self._set_e2e_device_signing_key_txn,
|
||||
user_id, key_type, key
|
||||
)
|
||||
|
||||
def _get_e2e_device_signing_key_txn(self, txn, user_id, key_type, from_user_id=None):
|
||||
"""Returns a user's cross-signing key.
|
||||
|
||||
Args:
|
||||
txn (twisted.enterprise.adbapi.Connection): db connection
|
||||
user_id (str): the user whose key is being requested
|
||||
key_type (str): the type of key that is being set: either 'master'
|
||||
for a master key, 'self_signing' for a self-signing key, or
|
||||
'user_signing' for a user-signing key
|
||||
from_user_id (str): if specified, signatures made by this user on
|
||||
the key will be included in the result
|
||||
|
||||
Returns:
|
||||
dict of the key data
|
||||
"""
|
||||
sql = (
|
||||
"SELECT keydata "
|
||||
" FROM e2e_device_signing_keys "
|
||||
" WHERE user_id = ? AND keytype = ? ORDER BY ts DESC LIMIT 1"
|
||||
)
|
||||
txn.execute(sql, (user_id, key_type))
|
||||
row = txn.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
key = json.loads(row[0])
|
||||
|
||||
device_id = None
|
||||
for k in key["keys"].values():
|
||||
device_id = k
|
||||
|
||||
if from_user_id is not None:
|
||||
sql = (
|
||||
"SELECT key_id, signature "
|
||||
" FROM e2e_device_signatures "
|
||||
" WHERE user_id = ? "
|
||||
" AND target_user_id = ? "
|
||||
" AND target_device_id = ? "
|
||||
)
|
||||
txn.execute(sql, (from_user_id, user_id, device_id))
|
||||
row = txn.fetchone()
|
||||
if row:
|
||||
key.setdefault("signatures", {}) \
|
||||
.setdefault(from_user_id, {})[row[0]] = row[1]
|
||||
|
||||
return key
|
||||
|
||||
def get_e2e_cross_signing_key(self, user_id, key_type, from_user_id=None):
|
||||
"""Returns a user's cross-signing key.
|
||||
|
||||
Args:
|
||||
user_id (str): the user whose self-signing key is being requested
|
||||
key_type (str): the type of cross-signing key to get
|
||||
from_user_id (str): if specified, signatures made by this user on
|
||||
the self-signing key will be included in the result
|
||||
|
||||
Returns:
|
||||
dict of the key data
|
||||
"""
|
||||
return self.runInteraction(
|
||||
"get_e2e_device_signing_key",
|
||||
self._get_e2e_device_signing_key_txn,
|
||||
user_id, key_type, from_user_id
|
||||
)
|
||||
|
||||
def store_e2e_device_signatures(self, user_id, signatures):
|
||||
"""Stores cross-signing signatures.
|
||||
|
||||
Args:
|
||||
user_id (str): the user who made the signatures
|
||||
signatures (iterable[(str, str, str, str)]): signatures to add - each
|
||||
a tuple of (key_id, target_user_id, target_device_id, signature),
|
||||
where key_id is the ID of the key (including the signature
|
||||
algorithm) that made the signature, target_user_id and
|
||||
target_device_id indicate the device being signed, and signature
|
||||
is the signature of the device
|
||||
"""
|
||||
return self._simple_insert_many(
|
||||
"e2e_device_signatures",
|
||||
[{"user_id": user_id,
|
||||
"key_id": key_id,
|
||||
"target_user_id": target_user_id,
|
||||
"target_device_id": target_device_id,
|
||||
"signature": signature}
|
||||
for (key_id, target_user_id, target_device_id, signature) in signatures],
|
||||
"add_e2e_signing_key"
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Remember to update this number every time a change is made to database
|
||||
# schema files, so the users will be informed on server restarts.
|
||||
SCHEMA_VERSION = 54
|
||||
SCHEMA_VERSION = 55
|
||||
|
||||
dir_path = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
48
synapse/storage/schema/delta/55/signing_keys.sql
Normal file
48
synapse/storage/schema/delta/55/signing_keys.sql
Normal file
@@ -0,0 +1,48 @@
|
||||
/* Copyright 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
-- device signing keys for cross-signing
|
||||
CREATE TABLE e2e_device_signing_keys (
|
||||
user_id TEXT NOT NULL,
|
||||
keytype TEXT NOT NULL,
|
||||
keydata TEXT NOT NULL,
|
||||
ts BIGINT NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX e2e_device_signing_keys_idx ON e2e_device_signing_keys(user_id, keytype, ts);
|
||||
|
||||
-- devices signatures for cross-signing
|
||||
CREATE TABLE e2e_device_signatures (
|
||||
user_id TEXT NOT NULL,
|
||||
key_id TEXT NOT NULL,
|
||||
target_user_id TEXT NOT NULL,
|
||||
target_device_id TEXT NOT NULL,
|
||||
signature TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX e2e_device_signatures_idx ON e2e_device_signatures(user_id, target_user_id, target_device_id);
|
||||
|
||||
-- stream of user signature updates
|
||||
CREATE TABLE user_signature_stream (
|
||||
stream_id BIGINT NOT NULL,
|
||||
from_user_id TEXT NOT NULL,
|
||||
user_ids TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX user_signature_stream_idx ON user_signature_stream(stream_id, from_user_id);
|
||||
|
||||
-- device list needs to know which ones are "real" devices, and which ones are
|
||||
-- just used to avoid collisions
|
||||
ALTER TABLE devices ADD COLUMN hidden BOOLEAN DEFAULT FALSE;
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014-2016 OpenMarket 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.
|
||||
@@ -17,6 +18,8 @@ import string
|
||||
from collections import namedtuple
|
||||
|
||||
import attr
|
||||
from signedjson.key import decode_verify_key_bytes
|
||||
from unpaddedbase64 import decode_base64
|
||||
|
||||
from synapse.api.errors import SynapseError
|
||||
|
||||
@@ -467,3 +470,30 @@ class ReadReceipt(object):
|
||||
user_id = attr.ib()
|
||||
event_ids = attr.ib()
|
||||
data = attr.ib()
|
||||
|
||||
|
||||
def get_verify_key_from_cross_signing_key(key_info):
|
||||
"""Get the key ID and signedjson verify key from a cross-signing key dict
|
||||
|
||||
Args:
|
||||
key_info (dict): a cross-signing key dict, which must have a "keys"
|
||||
property that has exactly one item in it
|
||||
|
||||
Returns:
|
||||
(str, VerifyKey): the key ID and verify key for the cross-signing key
|
||||
"""
|
||||
# make sure that exactly one key is provided
|
||||
if "keys" not in key_info:
|
||||
raise SynapseError(
|
||||
400,
|
||||
"Invalid key"
|
||||
)
|
||||
keys = key_info["keys"]
|
||||
if len(keys) != 1:
|
||||
raise SynapseError(
|
||||
400,
|
||||
"Invalid key"
|
||||
)
|
||||
# and return that one key
|
||||
for key_id, key_data in keys.items():
|
||||
return (key_id, decode_verify_key_bytes(key_id, decode_base64(key_data)))
|
||||
|
||||
Reference in New Issue
Block a user