Skip to content

Commit d2a966f

Browse files
turt2livekegsayMadLittleMods
authored
Use signature support from policy servers when available (#18934)
Opening on Kegan's behalf [MSC4284](matrix-org/matrix-spec-proposals#4284) has already been opened accordingly. --------- Co-authored-by: Kegan Dougal <[email protected]> Co-authored-by: Eric Eastwood <[email protected]>
1 parent dee6ba5 commit d2a966f

File tree

7 files changed

+421
-14
lines changed

7 files changed

+421
-14
lines changed

changelog.d/18934.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Update [MSC4284: Policy Servers](https://github.com/matrix-org/matrix-spec-proposals/pull/4284) implementation to support signatures when available.

synapse/crypto/keyring.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ async def process_request(self, verify_request: VerifyJsonRequest) -> None:
316316
if key_result.valid_until_ts < verify_request.minimum_valid_until_ts:
317317
continue
318318

319-
await self._process_json(key_result.verify_key, verify_request)
319+
await self.process_json(key_result.verify_key, verify_request)
320320
verified = True
321321

322322
if not verified:
@@ -326,7 +326,7 @@ async def process_request(self, verify_request: VerifyJsonRequest) -> None:
326326
Codes.UNAUTHORIZED,
327327
)
328328

329-
async def _process_json(
329+
async def process_json(
330330
self, verify_key: VerifyKey, verify_request: VerifyJsonRequest
331331
) -> None:
332332
"""Processes the `VerifyJsonRequest`. Raises if the signature can't be

synapse/federation/federation_client.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,43 @@ async def get_pdu_policy_recommendation(
495495
)
496496
return RECOMMENDATION_OK
497497

498+
@trace
499+
@tag_args
500+
async def ask_policy_server_to_sign_event(
501+
self, destination: str, pdu: EventBase, timeout: Optional[int] = None
502+
) -> Optional[JsonDict]:
503+
"""Requests that the destination server (typically a policy server)
504+
sign the event as not spam.
505+
506+
If the policy server could not be contacted or the policy server
507+
returned an error, this returns no signature.
508+
509+
Args:
510+
destination: The remote homeserver to ask (a policy server)
511+
pdu: The event to sign
512+
timeout: How long to try (in ms) the destination for before
513+
giving up. None indicates no timeout.
514+
Returns:
515+
The signature from the policy server, structured in the same was as the 'signatures'
516+
JSON in the event e.g { "$policy_server_via_domain" : { "ed25519:policy_server": "signature_base64" }}
517+
"""
518+
logger.debug(
519+
"ask_policy_server_to_sign_event for event_id=%s from %s",
520+
pdu.event_id,
521+
destination,
522+
)
523+
try:
524+
return await self.transport_layer.ask_policy_server_to_sign_event(
525+
destination, pdu, timeout=timeout
526+
)
527+
except Exception as e:
528+
logger.warning(
529+
"ask_policy_server_to_sign_event: server %s responded with error: %s",
530+
destination,
531+
e,
532+
)
533+
return None
534+
498535
@trace
499536
@tag_args
500537
async def get_pdu(

synapse/federation/transport/client.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,32 @@ async def get_policy_recommendation_for_pdu(
170170
timeout=timeout,
171171
)
172172

173+
async def ask_policy_server_to_sign_event(
174+
self, destination: str, event: EventBase, timeout: Optional[int] = None
175+
) -> JsonDict:
176+
"""Requests that the destination server (typically a policy server)
177+
sign the event as not spam.
178+
179+
If the policy server could not be contacted or the policy server
180+
returned an error, this raises that error.
181+
182+
Args:
183+
destination: The host name of the policy server / homeserver.
184+
event: The event to sign.
185+
timeout: How long to try (in ms) the destination for before giving up.
186+
None indicates no timeout.
187+
Returns:
188+
The signature from the policy server, structured in the same was as the 'signatures'
189+
JSON in the event e.g { "$policy_server_via_domain" : { "ed25519:policy_server": "signature_base64" }}
190+
"""
191+
return await self.client.post_json(
192+
destination=destination,
193+
path="/_matrix/policy/unstable/org.matrix.msc4284/sign",
194+
data=event.get_pdu_json(),
195+
ignore_backoff=True,
196+
timeout=timeout,
197+
)
198+
173199
async def backfill(
174200
self, destination: str, room_id: str, event_tuples: Collection[str], limit: int
175201
) -> Optional[Union[JsonDict, list]]:

synapse/handlers/message.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,12 @@ async def _create_and_send_nonmember_event_locked(
11381138
assert self.hs.is_mine_id(event.sender), "User must be our own: %s" % (
11391139
event.sender,
11401140
)
1141+
# if this room uses a policy server, try to get a signature now.
1142+
# We use verify=False here as we are about to call is_event_allowed on the same event
1143+
# which will do sig checks.
1144+
await self._policy_handler.ask_policy_server_to_sign_event(
1145+
event, verify=False
1146+
)
11411147

11421148
policy_allowed = await self._policy_handler.is_event_allowed(event)
11431149
if not policy_allowed:

synapse/handlers/room_policy.py

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
import logging
1818
from typing import TYPE_CHECKING
1919

20+
from signedjson.key import decode_verify_key_bytes
21+
from unpaddedbase64 import decode_base64
22+
23+
from synapse.api.errors import SynapseError
24+
from synapse.crypto.keyring import VerifyJsonRequest
2025
from synapse.events import EventBase
2126
from synapse.types.handlers.policy_server import RECOMMENDATION_OK
2227
from synapse.util.stringutils import parse_and_validate_server_name
@@ -26,6 +31,9 @@
2631

2732
logger = logging.getLogger(__name__)
2833

34+
POLICY_SERVER_EVENT_TYPE = "org.matrix.msc4284.policy"
35+
POLICY_SERVER_KEY_ID = "ed25519:policy_server"
36+
2937

3038
class RoomPolicyHandler:
3139
def __init__(self, hs: "HomeServer"):
@@ -54,11 +62,11 @@ async def is_event_allowed(self, event: EventBase) -> bool:
5462
Returns:
5563
bool: True if the event is allowed in the room, False otherwise.
5664
"""
57-
if event.type == "org.matrix.msc4284.policy" and event.state_key is not None:
65+
if event.type == POLICY_SERVER_EVENT_TYPE and event.state_key is not None:
5866
return True # always allow policy server change events
5967

6068
policy_event = await self._storage_controllers.state.get_current_state_event(
61-
event.room_id, "org.matrix.msc4284.policy", ""
69+
event.room_id, POLICY_SERVER_EVENT_TYPE, ""
6270
)
6371
if not policy_event:
6472
return True # no policy server == default allow
@@ -81,6 +89,22 @@ async def is_event_allowed(self, event: EventBase) -> bool:
8189
if not is_in_room:
8290
return True # policy server not in room == default allow
8391

92+
# Check if the event has been signed with the public key in the policy server state event.
93+
# If it is, we can save an HTTP hit.
94+
# We actually want to get the policy server state event BEFORE THE EVENT rather than
95+
# the current state value, else changing the public key will cause all of these checks to fail.
96+
# However, if we are checking outlier events (which we will due to is_event_allowed being called
97+
# near the edges at _check_sigs_and_hash) we won't know the state before the event, so the
98+
# only safe option is to use the current state
99+
public_key = policy_event.content.get("public_key", None)
100+
if public_key is not None and isinstance(public_key, str):
101+
valid = await self._verify_policy_server_signature(
102+
event, policy_server, public_key
103+
)
104+
if valid:
105+
return True
106+
# fallthrough to hit /check manually
107+
84108
# At this point, the server appears valid and is in the room, so ask it to check
85109
# the event.
86110
recommendation = await self._federation_client.get_pdu_policy_recommendation(
@@ -90,3 +114,73 @@ async def is_event_allowed(self, event: EventBase) -> bool:
90114
return False
91115

92116
return True # default allow
117+
118+
async def _verify_policy_server_signature(
119+
self, event: EventBase, policy_server: str, public_key: str
120+
) -> bool:
121+
# check the event is signed with this (via, public_key).
122+
verify_json_req = VerifyJsonRequest.from_event(policy_server, event, 0)
123+
try:
124+
key_bytes = decode_base64(public_key)
125+
verify_key = decode_verify_key_bytes(POLICY_SERVER_KEY_ID, key_bytes)
126+
# We would normally use KeyRing.verify_event_for_server but we can't here as we don't
127+
# want to fetch the server key, and instead want to use the public key in the state event.
128+
await self._hs.get_keyring().process_json(verify_key, verify_json_req)
129+
# if the event is correctly signed by the public key in the policy server state event = Allow
130+
return True
131+
except Exception as ex:
132+
logger.warning(
133+
"failed to verify event using public key in policy server event: %s", ex
134+
)
135+
return False
136+
137+
async def ask_policy_server_to_sign_event(
138+
self, event: EventBase, verify: bool = False
139+
) -> None:
140+
"""Ask the policy server to sign this event. The signature is added to the event signatures block.
141+
142+
Does nothing if there is no policy server state event in the room. If the policy server
143+
refuses to sign the event (as it's marked as spam) does nothing.
144+
145+
Args:
146+
event: The event to sign
147+
verify: If True, verify that the signature is correctly signed by the public_key in the
148+
policy server state event.
149+
Raises:
150+
if verify=True and the policy server signed the event with an invalid signature. Does
151+
not raise if the policy server refuses to sign the event.
152+
"""
153+
policy_event = await self._storage_controllers.state.get_current_state_event(
154+
event.room_id, POLICY_SERVER_EVENT_TYPE, ""
155+
)
156+
if not policy_event:
157+
return
158+
policy_server = policy_event.content.get("via", None)
159+
if policy_server is None or not isinstance(policy_server, str):
160+
return
161+
# Only ask to sign events if the policy state event has a public_key (so they can be subsequently verified)
162+
public_key = policy_event.content.get("public_key", None)
163+
if public_key is None or not isinstance(public_key, str):
164+
return
165+
166+
# Ask the policy server to sign this event.
167+
# We set a smallish timeout here as we don't want to block event sending too long.
168+
signature = await self._federation_client.ask_policy_server_to_sign_event(
169+
policy_server,
170+
event,
171+
timeout=3000,
172+
)
173+
if (
174+
# the policy server returns {} if it refuses to sign the event.
175+
signature and len(signature) > 0
176+
):
177+
event.signatures.update(signature)
178+
if verify:
179+
is_valid = await self._verify_policy_server_signature(
180+
event, policy_server, public_key
181+
)
182+
if not is_valid:
183+
raise SynapseError(
184+
500,
185+
f"policy server {policy_server} failed to sign event correctly",
186+
)

0 commit comments

Comments
 (0)