Validate policy server response and prevent overwriting signatures

This commit is contained in:
Olivier 'reivilibre
2026-05-27 12:10:05 +01:00
parent 407ac455ac
commit 6db51683d4
4 changed files with 64 additions and 5 deletions
+3 -2
View File
@@ -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
+10 -3
View File
@@ -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
+51
View File
@@ -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:
# <https://www.gnu.org/licenses/agpl_3.0.html>.
#
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 Servers 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