Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
326 changes: 326 additions & 0 deletions synapse/handlers/e2e_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
from six import iteritems

from canonicaljson import encode_canonical_json, json
from signedjson.key import decode_verify_key_bytes
from signedjson.sign import SignatureVerifyException, verify_signed_json
from unpaddedbase64 import decode_base64

from twisted.internet import defer

Expand Down Expand Up @@ -608,6 +610,279 @@ def upload_signing_keys_for_user(self, user_id, keys):

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
Comment thread
uhoreg marked this conversation as resolved.
devices to signed keys
"""
Comment thread
uhoreg marked this conversation as resolved.
failures = {}

# signatures to be stored. Each item will be a tuple of
# (signing_key_id, target_user_id, target_device_id, signature)
Comment thread
uhoreg marked this conversation as resolved.
Outdated
signature_list = []

# split between checking signatures for own user and signatures for
# other users, since we verify them with different keys
self_signatures = signatures.get(user_id, {})
other_signatures = {k: v for k, v in signatures.items() if k != user_id}

self_signature_list, self_failures = yield self._process_self_signatures(
user_id, self_signatures
)
signature_list.extend(self_signature_list)
failures.update(self_failures)

other_signature_list, other_failures = yield self._process_other_signatures(
user_id, other_signatures
)
signature_list.extend(other_signature_list)
failures.update(other_failures)

# store the signature, and send the appropriate notifications for sync
logger.debug("upload signature failures: %r", failures)
yield self.store.store_e2e_cross_signing_signatures(user_id, signature_list)

self_device_ids = [device_id for (_, _, device_id, _) in self_signature_list]
if self_device_ids:
yield self.device_handler.notify_device_update(user_id, self_device_ids)
signed_users = [user_id for (_, user_id, _, _) in other_signature_list]
if signed_users:
yield self.device_handler.notify_user_signature_update(
user_id, signed_users
)

return {"failures": failures}

@defer.inlineCallbacks
def _process_self_signatures(self, user_id, signatures):
"""Process uploaded signatures of the user's own keys.

Args:
user_id (string): the user uploading the keys
signatures (dict[string, dict]): map of devices to signed keys

Returns:
(list[(string, string, string, string)], dict[string, dict[string, dict]]):
a list of signatures to upload, in the form (signing_key_id, target_user_id,
target_device_id, signature), and a map of users to devices to failure
reasons
"""
signature_list = []
failures = {}
if not signatures:
return signature_list, failures

try:
# get our self-signing key to verify the signatures
self_signing_key, self_signing_key_id, self_signing_verify_key = yield self._get_e2e_cross_signing_verify_key(
Comment thread
uhoreg marked this conversation as resolved.
Outdated
user_id, "self_signing"
)

# get our master key, since it may be signed
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does "it" refer to ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"it" refers to the master key

master_key, master_key_id, master_verify_key = yield self._get_e2e_cross_signing_verify_key(
user_id, "master"
)

# fetch our stored devices. This is used to 1. verify
# signatures on the master key, and 2. to can compare with what
Comment thread
uhoreg marked this conversation as resolved.
Outdated
# was sent if the device was signed
devices = yield self.store.get_e2e_device_keys([(user_id, None)])

if user_id not in devices:
raise SynapseError(404, "No device keys found", Codes.NOT_FOUND)

devices = devices[user_id]
except SynapseError as e:
failures[user_id] = {
device: _exception_to_failure(e) for device in signatures.keys()
Comment thread
uhoreg marked this conversation as resolved.
Outdated
}
return signature_list, failures

for device_id, device in signatures.items():
try:
if "signatures" not in device or user_id not in device["signatures"]:
# no signature was sent
raise SynapseError(
400, "Invalid signature", Codes.INVALID_SIGNATURE
)

if device_id == master_verify_key.version:
Comment thread
richvdh marked this conversation as resolved.
# we have master key signed by devices: for each
Comment thread
uhoreg marked this conversation as resolved.
Outdated
# device that signed, check the signature. Since
# the "failures" property in the response only has
# granularity up to the signed device, either all
# of the signatures on the master key succeed, or
# all fail. So loop over the signatures and add
# them to a separate signature list. If everything
# works out, then add them all to the main
# signature list. (In practice, we're likely to
# only have only one signature anyways.)
master_key_signature_list = []
sigs = device["signatures"]
for signing_key_id, signature in sigs[user_id].items():
alg, signing_device_id = signing_key_id.split(":", 1)
Comment thread
uhoreg marked this conversation as resolved.
Outdated
if (
signing_device_id not in devices
or signing_key_id
not in devices[signing_device_id]["keys"]["keys"]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ugh, ["keys"]["keys"] is horrible, as is the stored_device = devices[device_id]["keys"] below. Any chance I can get you to take on a preliminary refactor of get_e2e_device_keys to return something slightly saner?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do so, if you have suggestions for how to fix it. It looks like get_e2e_device_keys is only used here and in query_local_devices. There, it needs device_info["keys"] and device_info["device_display_name"] and modifies device_info["keys"]to add the display name. Should I just make query_local_devices in change of adding in the device display name, and then have devices[signing_device_id]["keys"] become just devices[signing_device_id]?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that sounds sensible, I think. I'd love it if it could be a preliminary PR rather than rolled into this one, though.

):
# signed by an unknown device, or the
# device does not have the key
raise SynapseError(
400, "Invalid signature", Codes.INVALID_SIGNATURE
)

# get the key and check the signature
pubkey = devices[signing_device_id]["keys"]["keys"][
signing_key_id
]
verify_key = decode_verify_key_bytes(
signing_key_id, decode_base64(pubkey)
)
_check_device_signature(user_id, verify_key, device, master_key)
device["signatures"] = sigs
Comment thread
uhoreg marked this conversation as resolved.
Outdated

master_key_signature_list.append(
(signing_key_id, user_id, device_id, signature)
)

signature_list.extend(master_key_signature_list)
continue

# at this point, we have a device that should be signed
# by the self-signing key
if self_signing_key_id not in device["signatures"][user_id]:
# no signature was sent
raise SynapseError(
400, "Invalid signature", Codes.INVALID_SIGNATURE
)

stored_device = None
try:
stored_device = devices[device_id]["keys"]
except KeyError:
raise SynapseError(404, "Unknown device", Codes.NOT_FOUND)
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)

return signature_list, failures

@defer.inlineCallbacks
def _process_other_signatures(self, user_id, signatures):
"""Process uploaded signatures of other users' keys.

Args:
user_id (string): the user uploading the keys
signatures (dict[string, dict]): map of users to devices to signed keys

Returns:
(list[(string, string, string, string)], dict[string, dict[string, dict]]):
a list of signatures to upload, in the form (signing_key_id, target_user_id,
target_device_id, signature), and a map of users to devices to failure
reasons
"""
signature_list = []
failures = {}
if not signatures:
return signature_list, failures

try:
# get our user-signing key to verify the signatures
user_signing_key, user_signing_key_id, user_signing_verify_key = yield self._get_e2e_cross_signing_verify_key(
user_id, "user_signing"
)
except SynapseError as e:
failure = _exception_to_failure(e)
for user, devicemap in signatures.items():
failures[user] = {device_id: failure for device_id in devicemap.keys()}
return signature_list, failures

for user, devicemap in signatures.items():
Comment thread
uhoreg marked this conversation as resolved.
Outdated
device_id = None
try:
# get the user's master key, to make sure it matches
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to make sure it matches what was sent

don't we rather need to get it to check the signature?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, in this function, we checking signatures on other users' master keys, made by our user-signing key. (I'm adding a note in the docstring.)

# what was sent
stored_key, stored_key_id, _ = yield self._get_e2e_cross_signing_verify_key(
Comment thread
uhoreg marked this conversation as resolved.
Outdated
user, "master", user_id
)

# make sure that the user's master key is the one that
# was signed (and no others)
device_id = stored_key_id.split(":", 1)[1]
if device_id not in devicemap:
Comment thread
richvdh marked this conversation as resolved.
logger.error(
"upload signature: could not find signature for device %s",
device_id,
)
# set device to None so that the failure gets
# marked on all the signatures
device_id = None
raise SynapseError(404, "Unknown device", Codes.NOT_FOUND)
key = devicemap[device_id]
other_devices = [k for k in devicemap.keys() if k != device_id]
if other_devices:
# other devices were signed -- mark those as failures
logger.error("upload signature: too many devices specified")
failure = _exception_to_failure(
SynapseError(404, "Unknown device", Codes.NOT_FOUND)
)
failures[user] = {device: failure for device in other_devices}

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]
signature_list.append((user_signing_key_id, user, device_id, signature))
except SynapseError as e:
failure = _exception_to_failure(e)
if device_id is None:
failures[user] = {
device_id: failure for device_id in devicemap.keys()
}
else:
failures.setdefault(user, {})[device_id] = failure

return signature_list, failures

@defer.inlineCallbacks
def _get_e2e_cross_signing_verify_key(self, user_id, key_type, from_user_id=None):
Comment thread
uhoreg marked this conversation as resolved.
key = yield self.store.get_e2e_cross_signing_key(
user_id, key_type, from_user_id
)
if key is None:
logger.error("no %s key found for %s", key_type, user_id)
Comment thread
uhoreg marked this conversation as resolved.
Outdated
raise SynapseError(
Comment thread
uhoreg marked this conversation as resolved.
Outdated
404, "No %s key found for %s" % (key_type, user_id), Codes.NOT_FOUND
)
key_id, verify_key = get_verify_key_from_cross_signing_key(key)
return key, key_id, verify_key


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
Expand Down Expand Up @@ -636,7 +911,58 @@ 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
Comment thread
richvdh marked this conversation as resolved.
Outdated
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"]
Comment thread
uhoreg marked this conversation as resolved.
Outdated
# use pop to avoid exception if key doesn't exist
signed_device.pop("unsigned", None)
stored_device.pop("signatures", None)
stored_device.pop("unsigned", None)
if signed_device != stored_device:
Comment thread
richvdh marked this conversation as resolved.
Outdated
logger.error(
"upload signatures: key does not match %s vs %s",
signed_device,
stored_device,
)
raise SynapseError(400, "Key does not match")

# 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, SynapseError):
return {"status": e.code, "errcode": e.errcode, "message": str(e)}

if isinstance(e, CodeMessageException):
return {"status": e.code, "message": str(e)}

Expand Down
Loading