diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 80138f8438..b156a7ff75 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -473,7 +473,7 @@ class FederationClient(FederationBase): @tag_args async def ask_policy_server_to_sign_event( self, destination: str, pdu: EventBase, timeout: int | None = None - ) -> JsonDict: + ) -> PolicySignResponse: """Requests that the destination server (typically a policy server) sign the event as not spam. @@ -494,9 +494,10 @@ class FederationClient(FederationBase): pdu.event_id, destination, ) - return await self.transport_layer.ask_policy_server_to_sign_event( + json_response = await self.transport_layer.ask_policy_server_to_sign_event( destination, pdu, timeout=timeout ) + return validate_response(json_response, PolicySignResponse) @trace @tag_args diff --git a/synapse/handlers/room_policy.py b/synapse/handlers/room_policy.py index e46e6dc2ef..5c9487d617 100644 --- a/synapse/handlers/room_policy.py +++ b/synapse/handlers/room_policy.py @@ -225,7 +225,7 @@ class RoomPolicyHandler: # Ask the policy server to sign this event. try: - signature = await self._federation_client.ask_policy_server_to_sign_event( + sign_response = await self._federation_client.ask_policy_server_to_sign_event( policy_server.server_name, event, # We set a smallish timeout here as we don't want to block event sending @@ -241,7 +241,7 @@ class RoomPolicyHandler: # also be implementation bugs. Whoever reads this when removing unstable MSC4284 # stuff, make a decision on whether to remove this bit. # https://github.com/element-hq/synapse/issues/19502 - if not signature or len(signature) == 0: + if len(sign_response.root) == 0: raise SynapseError( 403, "This event has been rejected as probable spam by the policy server", @@ -261,7 +261,14 @@ class RoomPolicyHandler: # servers need to manually fetch signatures for. This is the code that allows # those events to continue working (because they're legally sent, even if missing # the policy server signature). - event.signatures.update(signature) + for key_id, signature_b64 in sign_response.root.items(): + if ( + event.signatures.get_signature(policy_server.server_name, key_id) + is None + ): + event.signatures.add_signature( + policy_server.server_name, key_id, signature_b64 + ) except HttpResponseException as ex: # re-wrap HTTP errors as `SynapseError` so they can be proxied to clients directly raise ex.to_synapse_error() from ex diff --git a/synapse/types/federation/__init__.py b/synapse/types/federation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/synapse/types/federation/policy.py b/synapse/types/federation/policy.py new file mode 100644 index 0000000000..4f8600f516 --- /dev/null +++ b/synapse/types/federation/policy.py @@ -0,0 +1,51 @@ +# +# This file is licensed under the Affero General Public License (AGPL) version 3. +# +# Copyright (C) 2026 Element Creations Ltd. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# See the GNU Affero General Public License for more details: +# . +# +from pydantic import model_validator +from typing_extensions import Self + +from synapse.util.pydantic_models import StrictRootModel + + +class PolicySignResponse(StrictRootModel[dict[str, str]]): + """ + Response to `POST /_matrix/policy/v1/sign` + Spec: https://spec.matrix.org/v1.18/server-server-api/#post_matrixpolicyv1sign + + Example: + { + "policy.example.org": { + "ed25519:policy_server": "zLFxllD0pbBuBpfHh8NuHNaICpReF/PAOpUQTsw+bFGKiGfDNAsnhcP7pbrmhhpfbOAxIdLraQLeeiXBryLmBw" + } + } + """ + + @model_validator(mode="after") + def check_rules(self) -> Self: + """ + > The Policy Server has signed the event, indicating that it recommends the event for inclusion in the room. + > Only the Policy Server’s signature is returned. + > This signature is to be added to the event before sending or processing the event further. + > + > `ed25519:policy_server` is always used for Ed25519 signatures. + > — https://spec.matrix.org/v1.18/server-server-api/#validating-policy-server-signatures + """ + + for key_id in self.root.keys(): + if key_id.startswith("ed25519:") and key_id != "ed25519:policy_server": + # > `ed25519:policy_server` is always used for Ed25519 signatures. + raise ValueError( + "policy servers must only use ed25519:policy_server for Ed25519 signatures" + ) + + return self