mirror of
https://mau.dev/maunium/synapse.git
synced 2024-08-25 03:35:28 +00:00
Merge pull request #584 from matrix-org/daniel/ephemeralthreepids
Allow third_party_signed to be specified on /join
This commit is contained in:
commit
28ad246bb4
@ -434,31 +434,46 @@ class Auth(object):
|
|||||||
|
|
||||||
if event.user_id != invite_event.user_id:
|
if event.user_id != invite_event.user_id:
|
||||||
return False
|
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
|
if signed["mxid"] != event.state_key:
|
||||||
# correct server signed the signed bundle.
|
|
||||||
# The caller is responsible for checking that the signing
|
|
||||||
# server has not revoked that public key.
|
|
||||||
return True
|
|
||||||
return False
|
return False
|
||||||
except (KeyError, SignatureVerifyException,):
|
if signed["token"] != token:
|
||||||
return False
|
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):
|
def _get_power_level_event(self, auth_events):
|
||||||
key = (EventTypes.PowerLevels, "", )
|
key = (EventTypes.PowerLevels, "", )
|
||||||
return auth_events.get(key)
|
return auth_events.get(key)
|
||||||
|
@ -543,8 +543,19 @@ class FederationServer(FederationBase):
|
|||||||
return event
|
return event
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def exchange_third_party_invite(self, invite):
|
def exchange_third_party_invite(
|
||||||
ret = yield self.handler.exchange_third_party_invite(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.returnValue(ret)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
|
@ -425,7 +425,17 @@ class On3pidBindServlet(BaseFederationServlet):
|
|||||||
last_exception = None
|
last_exception = None
|
||||||
for invite in content["invites"]:
|
for invite in content["invites"]:
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
last_exception = e
|
last_exception = e
|
||||||
if last_exception:
|
if last_exception:
|
||||||
|
@ -14,6 +14,9 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
"""Contains handlers for federation events."""
|
"""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
|
from ._base import BaseHandler
|
||||||
|
|
||||||
@ -1620,19 +1623,15 @@ class FederationHandler(BaseHandler):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def exchange_third_party_invite(self, invite):
|
def exchange_third_party_invite(
|
||||||
sender = invite["sender"]
|
self,
|
||||||
room_id = invite["room_id"]
|
sender_user_id,
|
||||||
|
target_user_id,
|
||||||
if "signed" not in invite or "token" not in invite["signed"]:
|
room_id,
|
||||||
logger.info(
|
signed,
|
||||||
"Discarding received notification of third party invite "
|
):
|
||||||
"without signed: %s" % (invite,)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
third_party_invite = {
|
third_party_invite = {
|
||||||
"signed": invite["signed"],
|
"signed": signed,
|
||||||
}
|
}
|
||||||
|
|
||||||
event_dict = {
|
event_dict = {
|
||||||
@ -1642,8 +1641,8 @@ class FederationHandler(BaseHandler):
|
|||||||
"third_party_invite": third_party_invite,
|
"third_party_invite": third_party_invite,
|
||||||
},
|
},
|
||||||
"room_id": room_id,
|
"room_id": room_id,
|
||||||
"sender": sender,
|
"sender": sender_user_id,
|
||||||
"state_key": invite["mxid"],
|
"state_key": target_user_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (yield self.auth.check_host_in_room(room_id, self.hs.hostname)):
|
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)
|
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
|
member_handler = self.hs.get_handlers().room_member_handler
|
||||||
yield member_handler.send_membership_event(event, context, from_client=False)
|
yield member_handler.send_membership_event(event, context, from_client=False)
|
||||||
else:
|
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(
|
yield self.replication_layer.forward_third_party_invite(
|
||||||
destinations,
|
destinations,
|
||||||
room_id,
|
room_id,
|
||||||
@ -1681,7 +1680,7 @@ class FederationHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.auth.check(event, auth_events=context.current_state)
|
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)
|
returned_invite = yield self.send_invite(origin, event)
|
||||||
# TODO: Make sure the signatures actually are correct.
|
# TODO: Make sure the signatures actually are correct.
|
||||||
@ -1711,17 +1710,69 @@ class FederationHandler(BaseHandler):
|
|||||||
defer.returnValue((event, context))
|
defer.returnValue((event, context))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _validate_keyserver(self, event, auth_events):
|
def _check_signature(self, event, auth_events):
|
||||||
token = event.content["third_party_invite"]["signed"]["token"]
|
"""
|
||||||
|
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(
|
invite_event = auth_events.get(
|
||||||
(EventTypes.ThirdPartyInvite, token,)
|
(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:
|
try:
|
||||||
response = yield self.hs.get_simple_http_client().get_json(
|
response = yield self.hs.get_simple_http_client().get_json(
|
||||||
invite_event.content["key_validity_url"],
|
url,
|
||||||
{"public_key": invite_event.content["public_key"]}
|
{"public_key": public_key}
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
|
@ -398,6 +398,7 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
action,
|
action,
|
||||||
txn_id=None,
|
txn_id=None,
|
||||||
remote_room_hosts=None,
|
remote_room_hosts=None,
|
||||||
|
third_party_signed=None,
|
||||||
ratelimit=True,
|
ratelimit=True,
|
||||||
):
|
):
|
||||||
effective_membership_state = action
|
effective_membership_state = action
|
||||||
@ -406,6 +407,15 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
elif action == "forget":
|
elif action == "forget":
|
||||||
effective_membership_state = "leave"
|
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
|
msg_handler = self.hs.get_handlers().message_handler
|
||||||
|
|
||||||
content = {"membership": effective_membership_state}
|
content = {"membership": effective_membership_state}
|
||||||
@ -759,7 +769,7 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
if room_avatar_event:
|
if room_avatar_event:
|
||||||
room_avatar_url = room_avatar_event.content.get("url", "")
|
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(
|
yield self._ask_id_server_for_third_party_invite(
|
||||||
id_server=id_server,
|
id_server=id_server,
|
||||||
medium=medium,
|
medium=medium,
|
||||||
@ -774,14 +784,18 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
inviter_avatar_url=inviter_avatar_url
|
inviter_avatar_url=inviter_avatar_url
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
msg_handler = self.hs.get_handlers().message_handler
|
msg_handler = self.hs.get_handlers().message_handler
|
||||||
yield msg_handler.create_and_send_nonmember_event(
|
yield msg_handler.create_and_send_nonmember_event(
|
||||||
{
|
{
|
||||||
"type": EventTypes.ThirdPartyInvite,
|
"type": EventTypes.ThirdPartyInvite,
|
||||||
"content": {
|
"content": {
|
||||||
"display_name": display_name,
|
"display_name": display_name,
|
||||||
"key_validity_url": key_validity_url,
|
"public_keys": public_keys,
|
||||||
"public_key": public_key,
|
|
||||||
|
# For backwards compatibility:
|
||||||
|
"key_validity_url": fallback_public_key["key_validity_url"],
|
||||||
|
"public_key": fallback_public_key["public_key"],
|
||||||
},
|
},
|
||||||
"room_id": room_id,
|
"room_id": room_id,
|
||||||
"sender": user.to_string(),
|
"sender": user.to_string(),
|
||||||
@ -806,6 +820,34 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
inviter_display_name,
|
inviter_display_name,
|
||||||
inviter_avatar_url
|
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" % (
|
is_url = "%s%s/_matrix/identity/api/v1/store-invite" % (
|
||||||
id_server_scheme, id_server,
|
id_server_scheme, id_server,
|
||||||
)
|
)
|
||||||
@ -826,12 +868,21 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
# TODO: Check for success
|
# TODO: Check for success
|
||||||
token = data["token"]
|
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"]
|
display_name = data["display_name"]
|
||||||
key_validity_url = "%s%s/_matrix/identity/api/v1/pubkey/isvalid" % (
|
defer.returnValue((token, public_keys, fallback_public_key, display_name))
|
||||||
id_server_scheme, id_server,
|
|
||||||
)
|
|
||||||
defer.returnValue((token, public_key, key_validity_url, display_name))
|
|
||||||
|
|
||||||
def forget(self, user, room_id):
|
def forget(self, user, room_id):
|
||||||
return self.store.forget(user.to_string(), room_id)
|
return self.store.forget(user.to_string(), room_id)
|
||||||
|
@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
REQUIREMENTS = {
|
REQUIREMENTS = {
|
||||||
"frozendict>=0.4": ["frozendict"],
|
"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"],
|
"canonicaljson>=1.0.0": ["canonicaljson>=1.0.0"],
|
||||||
"signedjson>=1.0.0": ["signedjson>=1.0.0"],
|
"signedjson>=1.0.0": ["signedjson>=1.0.0"],
|
||||||
"pynacl==0.3.0": ["nacl==0.3.0", "nacl.bindings"],
|
"pynacl==0.3.0": ["nacl==0.3.0", "nacl.bindings"],
|
||||||
|
@ -228,6 +228,8 @@ class JoinRoomAliasServlet(ClientV1RestServlet):
|
|||||||
allow_guest=True,
|
allow_guest=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
content = _parse_json(request)
|
||||||
|
|
||||||
if RoomID.is_valid(room_identifier):
|
if RoomID.is_valid(room_identifier):
|
||||||
room_id = room_identifier
|
room_id = room_identifier
|
||||||
remote_room_hosts = None
|
remote_room_hosts = None
|
||||||
@ -248,6 +250,7 @@ class JoinRoomAliasServlet(ClientV1RestServlet):
|
|||||||
action="join",
|
action="join",
|
||||||
txn_id=txn_id,
|
txn_id=txn_id,
|
||||||
remote_room_hosts=remote_room_hosts,
|
remote_room_hosts=remote_room_hosts,
|
||||||
|
third_party_signed=content.get("third_party_signed", None),
|
||||||
)
|
)
|
||||||
|
|
||||||
defer.returnValue((200, {"room_id": room_id}))
|
defer.returnValue((200, {"room_id": room_id}))
|
||||||
@ -451,6 +454,7 @@ class RoomMembershipRestServlet(ClientV1RestServlet):
|
|||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
action=membership_action,
|
action=membership_action,
|
||||||
txn_id=txn_id,
|
txn_id=txn_id,
|
||||||
|
third_party_signed=content.get("third_party_signed", None),
|
||||||
)
|
)
|
||||||
|
|
||||||
defer.returnValue((200, {}))
|
defer.returnValue((200, {}))
|
||||||
|
Loading…
Reference in New Issue
Block a user