Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Tests: replace mocked Authenticator with the real thing #11913

Merged
merged 2 commits into from
Feb 11, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/11913.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Tests: replace mocked `Authenticator` with the real thing.
4 changes: 2 additions & 2 deletions tests/federation/test_complexity.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def test_complexity_simple(self):
)

# Get the room complexity
channel = self.make_request(
channel = self.make_signed_federation_request(
"GET", "/_matrix/federation/unstable/rooms/%s/complexity" % (room_1,)
)
self.assertEquals(200, channel.code)
Expand All @@ -59,7 +59,7 @@ def test_complexity_simple(self):
store.get_current_state_event_counts = lambda x: make_awaitable(500 * 1.23)

# Get the room complexity again -- make sure it's our artificial value
channel = self.make_request(
channel = self.make_signed_federation_request(
"GET", "/_matrix/federation/unstable/rooms/%s/complexity" % (room_1,)
)
self.assertEquals(200, channel.code)
Expand Down
4 changes: 2 additions & 2 deletions tests/federation/test_federation_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def test_without_event_id(self):
room_1 = self.helper.create_room_as(u1, tok=u1_token)
self.inject_room_member(room_1, "@user:other.example.com", "join")

channel = self.make_request(
channel = self.make_signed_federation_request(
"GET", "/_matrix/federation/v1/state/%s" % (room_1,)
)
self.assertEquals(200, channel.code, channel.result)
Expand Down Expand Up @@ -145,7 +145,7 @@ def test_needs_to_be_in_room(self):

room_1 = self.helper.create_room_as(u1, tok=u1_token)

channel = self.make_request(
channel = self.make_signed_federation_request(
"GET", "/_matrix/federation/v1/state/%s" % (room_1,)
)
self.assertEquals(403, channel.code, channel.result)
Expand Down
136 changes: 106 additions & 30 deletions tests/unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import hashlib
import hmac
import inspect
import json
import logging
import secrets
import time
Expand All @@ -36,9 +37,11 @@
)
from unittest.mock import Mock, patch

from canonicaljson import json
import canonicaljson
import signedjson.key
import unpaddedbase64

from twisted.internet.defer import Deferred, ensureDeferred, succeed
from twisted.internet.defer import Deferred, ensureDeferred
from twisted.python.failure import Failure
from twisted.python.threadpool import ThreadPool
from twisted.test.proto_helpers import MemoryReactor
Expand All @@ -49,8 +52,7 @@
from synapse import events
from synapse.api.constants import EventTypes, Membership
from synapse.config.homeserver import HomeServerConfig
from synapse.config.ratelimiting import FederationRateLimitConfig
from synapse.federation.transport import server as federation_server
from synapse.federation.transport.server import TransportLayerServer
from synapse.http.server import JsonResource
from synapse.http.site import SynapseRequest, SynapseSite
from synapse.logging.context import (
Expand All @@ -61,10 +63,10 @@
)
from synapse.rest import RegisterServletsFunc
from synapse.server import HomeServer
from synapse.storage.keys import FetchKeyResult
from synapse.types import JsonDict, UserID, create_requester
from synapse.util import Clock
from synapse.util.httpresourcetree import create_resource_tree
from synapse.util.ratelimitutils import FederationRateLimiter

from tests.server import FakeChannel, get_clock, make_request, setup_test_homeserver
from tests.test_utils import event_injection, setup_awaitable_errors
Expand Down Expand Up @@ -755,42 +757,116 @@ def inject_room_member(self, room: str, user: str, membership: Membership) -> No

class FederatingHomeserverTestCase(HomeserverTestCase):
"""
A federating homeserver that authenticates incoming requests as `other.example.com`.
A federating homeserver, set up to validate incoming federation requests
"""

def create_resource_dict(self) -> Dict[str, Resource]:
d = super().create_resource_dict()
d["/_matrix/federation"] = TestTransportLayerServer(self.hs)
return d
OTHER_SERVER_NAME = "other.example.com"
OTHER_SERVER_SIGNATURE_KEY = signedjson.key.generate_signing_key("test")

def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer):
super().prepare(reactor, clock, hs)

class TestTransportLayerServer(JsonResource):
"""A test implementation of TransportLayerServer
# poke the other server's signing key into the key store, so that we don't
# make requests for it
verify_key = signedjson.key.get_verify_key(self.OTHER_SERVER_SIGNATURE_KEY)
verify_key_id = "%s:%s" % (verify_key.alg, verify_key.version)

authenticates incoming requests as `other.example.com`.
"""
self.get_success(
hs.get_datastore().store_server_verify_keys(
from_server=self.OTHER_SERVER_NAME,
ts_added_ms=clock.time_msec(),
verify_keys=[
(
self.OTHER_SERVER_NAME,
verify_key_id,
FetchKeyResult(
verify_key=verify_key,
valid_until_ts=clock.time_msec() + 1000,
),
)
],
)
)

def create_resource_dict(self) -> Dict[str, Resource]:
d = super().create_resource_dict()
d["/_matrix/federation"] = TransportLayerServer(self.hs)
return d

def __init__(self, hs):
super().__init__(hs)
def make_signed_federation_request(
self,
method: str,
path: str,
content: Optional[JsonDict] = None,
await_result: bool = True,
custom_headers: Optional[Iterable[Tuple[AnyStr, AnyStr]]] = None,
client_ip: str = "127.0.0.1",
) -> FakeChannel:
"""Make an inbound signed federation request to this server

class Authenticator:
def authenticate_request(self, request, content):
return succeed("other.example.com")
The request is signed as if it came from "other.example.com", which our HS
already has the keys for.
"""

authenticator = Authenticator()
if custom_headers is None:
custom_headers = []
else:
custom_headers = list(custom_headers)

custom_headers.append(
(
"Authorization",
_auth_header_for_request(
origin=self.OTHER_SERVER_NAME,
destination=self.hs.hostname,
signing_key=self.OTHER_SERVER_SIGNATURE_KEY,
method=method,
path=path,
content=content,
),
)
)

ratelimiter = FederationRateLimiter(
hs.get_clock(),
FederationRateLimitConfig(
window_size=1,
sleep_limit=1,
sleep_delay=1,
reject_limit=1000,
concurrent=1000,
),
return make_request(
self.reactor,
self.site,
method=method,
path=path,
content=content,
shorthand=False,
await_result=await_result,
custom_headers=custom_headers,
client_ip=client_ip,
)

federation_server.register_servlets(hs, self, authenticator, ratelimiter)

def _auth_header_for_request(
origin: str,
destination: str,
signing_key: signedjson.key.SigningKey,
method: str,
path: str,
content: Optional[JsonDict],
) -> str:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly surprised we don't already have a function that we can reuse to do this, but 🤷

"""Build a suitable Authorization header for an outgoing federation request"""
request_description: JsonDict = {
"method": method,
"uri": path,
"destination": destination,
"origin": origin,
}
if content is not None:
request_description["content"] = content
signature_base64 = unpaddedbase64.encode_base64(
signing_key.sign(
canonicaljson.encode_canonical_json(request_description)
).signature
)
return (
f"X-Matrix origin={origin},"
f"key={signing_key.alg}:{signing_key.version},"
f"sig={signature_base64}"
)


def override_config(extra_config):
Expand Down