Merge pull request #584 from matrix-org/daniel/ephemeralthreepids
Allow third_party_signed to be specified on /joinpull/604/head
						commit
						28ad246bb4
					
				|  | @ -434,31 +434,46 @@ class Auth(object): | |||
| 
 | ||||
|         if event.user_id != invite_event.user_id: | ||||
|             return False | ||||
|         try: | ||||
|             public_key = invite_event.content["public_key"] | ||||
|             if signed["mxid"] != event.state_key: | ||||
|                 return False | ||||
|             if signed["token"] != token: | ||||
|                 return False | ||||
|             for server, signature_block in signed["signatures"].items(): | ||||
|                 for key_name, encoded_signature in signature_block.items(): | ||||
|                     if not key_name.startswith("ed25519:"): | ||||
|                         return False | ||||
|                     verify_key = decode_verify_key_bytes( | ||||
|                         key_name, | ||||
|                         decode_base64(public_key) | ||||
|                     ) | ||||
|                     verify_signed_json(signed, server, verify_key) | ||||
| 
 | ||||
|                     # We got the public key from the invite, so we know that the | ||||
|                     # correct server signed the signed bundle. | ||||
|                     # The caller is responsible for checking that the signing | ||||
|                     # server has not revoked that public key. | ||||
|                     return True | ||||
|         if signed["mxid"] != event.state_key: | ||||
|             return False | ||||
|         except (KeyError, SignatureVerifyException,): | ||||
|         if signed["token"] != token: | ||||
|             return False | ||||
| 
 | ||||
|         for public_key_object in self.get_public_keys(invite_event): | ||||
|             public_key = public_key_object["public_key"] | ||||
|             try: | ||||
|                 for server, signature_block in signed["signatures"].items(): | ||||
|                     for key_name, encoded_signature in signature_block.items(): | ||||
|                         if not key_name.startswith("ed25519:"): | ||||
|                             continue | ||||
|                         verify_key = decode_verify_key_bytes( | ||||
|                             key_name, | ||||
|                             decode_base64(public_key) | ||||
|                         ) | ||||
|                         verify_signed_json(signed, server, verify_key) | ||||
| 
 | ||||
|                         # We got the public key from the invite, so we know that the | ||||
|                         # correct server signed the signed bundle. | ||||
|                         # The caller is responsible for checking that the signing | ||||
|                         # server has not revoked that public key. | ||||
|                         return True | ||||
|             except (KeyError, SignatureVerifyException,): | ||||
|                 continue | ||||
|         return False | ||||
| 
 | ||||
|     def get_public_keys(self, invite_event): | ||||
|         public_keys = [] | ||||
|         if "public_key" in invite_event.content: | ||||
|             o = { | ||||
|                 "public_key": invite_event.content["public_key"], | ||||
|             } | ||||
|             if "key_validity_url" in invite_event.content: | ||||
|                 o["key_validity_url"] = invite_event.content["key_validity_url"] | ||||
|             public_keys.append(o) | ||||
|         public_keys.extend(invite_event.content.get("public_keys", [])) | ||||
|         return public_keys | ||||
| 
 | ||||
|     def _get_power_level_event(self, auth_events): | ||||
|         key = (EventTypes.PowerLevels, "", ) | ||||
|         return auth_events.get(key) | ||||
|  |  | |||
|  | @ -543,8 +543,19 @@ class FederationServer(FederationBase): | |||
|         return event | ||||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     def exchange_third_party_invite(self, invite): | ||||
|         ret = yield self.handler.exchange_third_party_invite(invite) | ||||
|     def exchange_third_party_invite( | ||||
|             self, | ||||
|             sender_user_id, | ||||
|             target_user_id, | ||||
|             room_id, | ||||
|             signed, | ||||
|     ): | ||||
|         ret = yield self.handler.exchange_third_party_invite( | ||||
|             sender_user_id, | ||||
|             target_user_id, | ||||
|             room_id, | ||||
|             signed, | ||||
|         ) | ||||
|         defer.returnValue(ret) | ||||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|  |  | |||
|  | @ -425,7 +425,17 @@ class On3pidBindServlet(BaseFederationServlet): | |||
|             last_exception = None | ||||
|             for invite in content["invites"]: | ||||
|                 try: | ||||
|                     yield self.handler.exchange_third_party_invite(invite) | ||||
|                     if "signed" not in invite or "token" not in invite["signed"]: | ||||
|                         message = ("Rejecting received notification of third-" | ||||
|                                    "party invite without signed: %s" % (invite,)) | ||||
|                         logger.info(message) | ||||
|                         raise SynapseError(400, message) | ||||
|                     yield self.handler.exchange_third_party_invite( | ||||
|                         invite["sender"], | ||||
|                         invite["mxid"], | ||||
|                         invite["room_id"], | ||||
|                         invite["signed"], | ||||
|                     ) | ||||
|                 except Exception as e: | ||||
|                     last_exception = e | ||||
|             if last_exception: | ||||
|  |  | |||
|  | @ -14,6 +14,9 @@ | |||
| # limitations under the License. | ||||
| 
 | ||||
| """Contains handlers for federation events.""" | ||||
| from signedjson.key import decode_verify_key_bytes | ||||
| from signedjson.sign import verify_signed_json | ||||
| from unpaddedbase64 import decode_base64 | ||||
| 
 | ||||
| from ._base import BaseHandler | ||||
| 
 | ||||
|  | @ -1620,19 +1623,15 @@ class FederationHandler(BaseHandler): | |||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     @log_function | ||||
|     def exchange_third_party_invite(self, invite): | ||||
|         sender = invite["sender"] | ||||
|         room_id = invite["room_id"] | ||||
| 
 | ||||
|         if "signed" not in invite or "token" not in invite["signed"]: | ||||
|             logger.info( | ||||
|                 "Discarding received notification of third party invite " | ||||
|                 "without signed: %s" % (invite,) | ||||
|             ) | ||||
|             return | ||||
| 
 | ||||
|     def exchange_third_party_invite( | ||||
|             self, | ||||
|             sender_user_id, | ||||
|             target_user_id, | ||||
|             room_id, | ||||
|             signed, | ||||
|     ): | ||||
|         third_party_invite = { | ||||
|             "signed": invite["signed"], | ||||
|             "signed": signed, | ||||
|         } | ||||
| 
 | ||||
|         event_dict = { | ||||
|  | @ -1642,8 +1641,8 @@ class FederationHandler(BaseHandler): | |||
|                 "third_party_invite": third_party_invite, | ||||
|             }, | ||||
|             "room_id": room_id, | ||||
|             "sender": sender, | ||||
|             "state_key": invite["mxid"], | ||||
|             "sender": sender_user_id, | ||||
|             "state_key": target_user_id, | ||||
|         } | ||||
| 
 | ||||
|         if (yield self.auth.check_host_in_room(room_id, self.hs.hostname)): | ||||
|  | @ -1656,11 +1655,11 @@ class FederationHandler(BaseHandler): | |||
|             ) | ||||
| 
 | ||||
|             self.auth.check(event, context.current_state) | ||||
|             yield self._validate_keyserver(event, auth_events=context.current_state) | ||||
|             yield self._check_signature(event, auth_events=context.current_state) | ||||
|             member_handler = self.hs.get_handlers().room_member_handler | ||||
|             yield member_handler.send_membership_event(event, context, from_client=False) | ||||
|         else: | ||||
|             destinations = set([x.split(":", 1)[-1] for x in (sender, room_id)]) | ||||
|             destinations = set(x.split(":", 1)[-1] for x in (sender_user_id, room_id)) | ||||
|             yield self.replication_layer.forward_third_party_invite( | ||||
|                 destinations, | ||||
|                 room_id, | ||||
|  | @ -1681,7 +1680,7 @@ class FederationHandler(BaseHandler): | |||
|         ) | ||||
| 
 | ||||
|         self.auth.check(event, auth_events=context.current_state) | ||||
|         yield self._validate_keyserver(event, auth_events=context.current_state) | ||||
|         yield self._check_signature(event, auth_events=context.current_state) | ||||
| 
 | ||||
|         returned_invite = yield self.send_invite(origin, event) | ||||
|         # TODO: Make sure the signatures actually are correct. | ||||
|  | @ -1711,17 +1710,69 @@ class FederationHandler(BaseHandler): | |||
|         defer.returnValue((event, context)) | ||||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     def _validate_keyserver(self, event, auth_events): | ||||
|         token = event.content["third_party_invite"]["signed"]["token"] | ||||
|     def _check_signature(self, event, auth_events): | ||||
|         """ | ||||
|         Checks that the signature in the event is consistent with its invite. | ||||
|         :param event (Event): The m.room.member event to check | ||||
|         :param auth_events (dict<(event type, state_key), event>) | ||||
| 
 | ||||
|         :raises | ||||
|             AuthError if signature didn't match any keys, or key has been | ||||
|                 revoked, | ||||
|             SynapseError if a transient error meant a key couldn't be checked | ||||
|                 for revocation. | ||||
|         """ | ||||
|         signed = event.content["third_party_invite"]["signed"] | ||||
|         token = signed["token"] | ||||
| 
 | ||||
|         invite_event = auth_events.get( | ||||
|             (EventTypes.ThirdPartyInvite, token,) | ||||
|         ) | ||||
| 
 | ||||
|         if not invite_event: | ||||
|             raise AuthError(403, "Could not find invite") | ||||
| 
 | ||||
|         last_exception = None | ||||
|         for public_key_object in self.hs.get_auth().get_public_keys(invite_event): | ||||
|             try: | ||||
|                 for server, signature_block in signed["signatures"].items(): | ||||
|                     for key_name, encoded_signature in signature_block.items(): | ||||
|                         if not key_name.startswith("ed25519:"): | ||||
|                             continue | ||||
| 
 | ||||
|                         public_key = public_key_object["public_key"] | ||||
|                         verify_key = decode_verify_key_bytes( | ||||
|                             key_name, | ||||
|                             decode_base64(public_key) | ||||
|                         ) | ||||
|                         verify_signed_json(signed, server, verify_key) | ||||
|                         if "key_validity_url" in public_key_object: | ||||
|                             yield self._check_key_revocation( | ||||
|                                 public_key, | ||||
|                                 public_key_object["key_validity_url"] | ||||
|                             ) | ||||
|                         return | ||||
|             except Exception as e: | ||||
|                 last_exception = e | ||||
|         raise last_exception | ||||
| 
 | ||||
|     @defer.inlineCallbacks | ||||
|     def _check_key_revocation(self, public_key, url): | ||||
|         """ | ||||
|         Checks whether public_key has been revoked. | ||||
| 
 | ||||
|         :param public_key (str): base-64 encoded public key. | ||||
|         :param url (str): Key revocation URL. | ||||
| 
 | ||||
|         :raises | ||||
|             AuthError if they key has been revoked. | ||||
|             SynapseError if a transient error meant a key couldn't be checked | ||||
|                 for revocation. | ||||
|         """ | ||||
|         try: | ||||
|             response = yield self.hs.get_simple_http_client().get_json( | ||||
|                 invite_event.content["key_validity_url"], | ||||
|                 {"public_key": invite_event.content["public_key"]} | ||||
|                 url, | ||||
|                 {"public_key": public_key} | ||||
|             ) | ||||
|         except Exception: | ||||
|             raise SynapseError( | ||||
|  |  | |||
|  | @ -398,6 +398,7 @@ class RoomMemberHandler(BaseHandler): | |||
|             action, | ||||
|             txn_id=None, | ||||
|             remote_room_hosts=None, | ||||
|             third_party_signed=None, | ||||
|             ratelimit=True, | ||||
|     ): | ||||
|         effective_membership_state = action | ||||
|  | @ -406,6 +407,15 @@ class RoomMemberHandler(BaseHandler): | |||
|         elif action == "forget": | ||||
|             effective_membership_state = "leave" | ||||
| 
 | ||||
|         if third_party_signed is not None: | ||||
|             replication = self.hs.get_replication_layer() | ||||
|             yield replication.exchange_third_party_invite( | ||||
|                 third_party_signed["sender"], | ||||
|                 target.to_string(), | ||||
|                 room_id, | ||||
|                 third_party_signed, | ||||
|             ) | ||||
| 
 | ||||
|         msg_handler = self.hs.get_handlers().message_handler | ||||
| 
 | ||||
|         content = {"membership": effective_membership_state} | ||||
|  | @ -759,7 +769,7 @@ class RoomMemberHandler(BaseHandler): | |||
|         if room_avatar_event: | ||||
|             room_avatar_url = room_avatar_event.content.get("url", "") | ||||
| 
 | ||||
|         token, public_key, key_validity_url, display_name = ( | ||||
|         token, public_keys, fallback_public_key, display_name = ( | ||||
|             yield self._ask_id_server_for_third_party_invite( | ||||
|                 id_server=id_server, | ||||
|                 medium=medium, | ||||
|  | @ -774,14 +784,18 @@ class RoomMemberHandler(BaseHandler): | |||
|                 inviter_avatar_url=inviter_avatar_url | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|         msg_handler = self.hs.get_handlers().message_handler | ||||
|         yield msg_handler.create_and_send_nonmember_event( | ||||
|             { | ||||
|                 "type": EventTypes.ThirdPartyInvite, | ||||
|                 "content": { | ||||
|                     "display_name": display_name, | ||||
|                     "key_validity_url": key_validity_url, | ||||
|                     "public_key": public_key, | ||||
|                     "public_keys": public_keys, | ||||
| 
 | ||||
|                     # For backwards compatibility: | ||||
|                     "key_validity_url": fallback_public_key["key_validity_url"], | ||||
|                     "public_key": fallback_public_key["public_key"], | ||||
|                 }, | ||||
|                 "room_id": room_id, | ||||
|                 "sender": user.to_string(), | ||||
|  | @ -806,6 +820,34 @@ class RoomMemberHandler(BaseHandler): | |||
|             inviter_display_name, | ||||
|             inviter_avatar_url | ||||
|     ): | ||||
|         """ | ||||
|         Asks an identity server for a third party invite. | ||||
| 
 | ||||
|         :param id_server (str): hostname + optional port for the identity server. | ||||
|         :param medium (str): The literal string "email". | ||||
|         :param address (str): The third party address being invited. | ||||
|         :param room_id (str): The ID of the room to which the user is invited. | ||||
|         :param inviter_user_id (str): The user ID of the inviter. | ||||
|         :param room_alias (str): An alias for the room, for cosmetic | ||||
|             notifications. | ||||
|         :param room_avatar_url (str): The URL of the room's avatar, for cosmetic | ||||
|             notifications. | ||||
|         :param room_join_rules (str): The join rules of the email | ||||
|             (e.g. "public"). | ||||
|         :param room_name (str): The m.room.name of the room. | ||||
|         :param inviter_display_name (str): The current display name of the | ||||
|             inviter. | ||||
|         :param inviter_avatar_url (str): The URL of the inviter's avatar. | ||||
| 
 | ||||
|         :return: A deferred tuple containing: | ||||
|             token (str): The token which must be signed to prove authenticity. | ||||
|             public_keys ([{"public_key": str, "key_validity_url": str}]): | ||||
|                 public_key is a base64-encoded ed25519 public key. | ||||
|             fallback_public_key: One element from public_keys. | ||||
|             display_name (str): A user-friendly name to represent the invited | ||||
|                 user. | ||||
|         """ | ||||
| 
 | ||||
|         is_url = "%s%s/_matrix/identity/api/v1/store-invite" % ( | ||||
|             id_server_scheme, id_server, | ||||
|         ) | ||||
|  | @ -826,12 +868,21 @@ class RoomMemberHandler(BaseHandler): | |||
|         ) | ||||
|         # TODO: Check for success | ||||
|         token = data["token"] | ||||
|         public_key = data["public_key"] | ||||
|         public_keys = data.get("public_keys", []) | ||||
|         if "public_key" in data: | ||||
|             fallback_public_key = { | ||||
|                 "public_key": data["public_key"], | ||||
|                 "key_validity_url": "%s%s/_matrix/identity/api/v1/pubkey/isvalid" % ( | ||||
|                     id_server_scheme, id_server, | ||||
|                 ), | ||||
|             } | ||||
|         else: | ||||
|             fallback_public_key = public_keys[0] | ||||
| 
 | ||||
|         if not public_keys: | ||||
|             public_keys.append(fallback_public_key) | ||||
|         display_name = data["display_name"] | ||||
|         key_validity_url = "%s%s/_matrix/identity/api/v1/pubkey/isvalid" % ( | ||||
|             id_server_scheme, id_server, | ||||
|         ) | ||||
|         defer.returnValue((token, public_key, key_validity_url, display_name)) | ||||
|         defer.returnValue((token, public_keys, fallback_public_key, display_name)) | ||||
| 
 | ||||
|     def forget(self, user, room_id): | ||||
|         return self.store.forget(user.to_string(), room_id) | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ logger = logging.getLogger(__name__) | |||
| 
 | ||||
| REQUIREMENTS = { | ||||
|     "frozendict>=0.4": ["frozendict"], | ||||
|     "unpaddedbase64>=1.0.1": ["unpaddedbase64>=1.0.1"], | ||||
|     "unpaddedbase64>=1.1.0": ["unpaddedbase64>=1.1.0"], | ||||
|     "canonicaljson>=1.0.0": ["canonicaljson>=1.0.0"], | ||||
|     "signedjson>=1.0.0": ["signedjson>=1.0.0"], | ||||
|     "pynacl==0.3.0": ["nacl==0.3.0", "nacl.bindings"], | ||||
|  |  | |||
|  | @ -228,6 +228,8 @@ class JoinRoomAliasServlet(ClientV1RestServlet): | |||
|             allow_guest=True, | ||||
|         ) | ||||
| 
 | ||||
|         content = _parse_json(request) | ||||
| 
 | ||||
|         if RoomID.is_valid(room_identifier): | ||||
|             room_id = room_identifier | ||||
|             remote_room_hosts = None | ||||
|  | @ -248,6 +250,7 @@ class JoinRoomAliasServlet(ClientV1RestServlet): | |||
|             action="join", | ||||
|             txn_id=txn_id, | ||||
|             remote_room_hosts=remote_room_hosts, | ||||
|             third_party_signed=content.get("third_party_signed", None), | ||||
|         ) | ||||
| 
 | ||||
|         defer.returnValue((200, {"room_id": room_id})) | ||||
|  | @ -451,6 +454,7 @@ class RoomMembershipRestServlet(ClientV1RestServlet): | |||
|             room_id=room_id, | ||||
|             action=membership_action, | ||||
|             txn_id=txn_id, | ||||
|             third_party_signed=content.get("third_party_signed", None), | ||||
|         ) | ||||
| 
 | ||||
|         defer.returnValue((200, {})) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Daniel Wagner-Hall
						Daniel Wagner-Hall