-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Cross-signing [3/4] -- uploading signatures edition #5726
Changes from 14 commits
4bb4544
ac4746a
7d6c70f
9061b41
5914fd0
c8dc740
e47af0f
369462d
561cbba
415d0a0
ab729e3
0d61d1d
b6e3dec
d3f2fbc
26113fb
39864f4
f4b6d43
c3635c9
125eb45
36adfae
6493ed5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
||
|
|
@@ -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 | ||
| devices to signed keys | ||
| """ | ||
|
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) | ||
|
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( | ||
|
uhoreg marked this conversation as resolved.
Outdated
|
||
| user_id, "self_signing" | ||
| ) | ||
|
|
||
| # get our master key, since it may be signed | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what does "it" refer to ?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
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() | ||
|
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: | ||
|
richvdh marked this conversation as resolved.
|
||
| # we have master key signed by devices: for each | ||
|
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) | ||
|
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"] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ugh,
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
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(): | ||
|
uhoreg marked this conversation as resolved.
Outdated
|
||
| device_id = None | ||
| try: | ||
| # get the user's master key, to make sure it matches | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
don't we rather need to get it to check the signature?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||
|
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: | ||
|
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): | ||
|
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) | ||
|
uhoreg marked this conversation as resolved.
Outdated
|
||
| raise SynapseError( | ||
|
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 | ||
|
|
@@ -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 | ||
|
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"] | ||
|
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: | ||
|
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)} | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.