123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275 |
- # Copyright 2020 The Matrix.org Foundation C.I.C.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- from io import BytesIO, StringIO
- from typing import Any, Dict, Optional, Union
- from unittest.mock import Mock
- import signedjson.key
- from canonicaljson import encode_canonical_json
- from signedjson.sign import sign_json
- from signedjson.types import SigningKey
- from twisted.test.proto_helpers import MemoryReactor
- from twisted.web.resource import NoResource, Resource
- from synapse.crypto.keyring import PerspectivesKeyFetcher
- from synapse.http.site import SynapseRequest
- from synapse.rest.key.v2 import KeyResource
- from synapse.server import HomeServer
- from synapse.storage.keys import FetchKeyResult
- from synapse.types import JsonDict
- from synapse.util import Clock
- from synapse.util.httpresourcetree import create_resource_tree
- from synapse.util.stringutils import random_string
- from tests import unittest
- from tests.server import FakeChannel
- from tests.utils import default_config
- class BaseRemoteKeyResourceTestCase(unittest.HomeserverTestCase):
- def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
- self.http_client = Mock()
- return self.setup_test_homeserver(federation_http_client=self.http_client)
- def create_test_resource(self) -> Resource:
- return create_resource_tree(
- {"/_matrix/key/v2": KeyResource(self.hs)}, root_resource=NoResource()
- )
- def expect_outgoing_key_request(
- self, server_name: str, signing_key: SigningKey
- ) -> None:
- """
- Tell the mock http client to expect an outgoing GET request for the given key
- """
- async def get_json(
- destination: str,
- path: str,
- ignore_backoff: bool = False,
- **kwargs: Any,
- ) -> Union[JsonDict, list]:
- self.assertTrue(ignore_backoff)
- self.assertEqual(destination, server_name)
- key_id = "%s:%s" % (signing_key.alg, signing_key.version)
- self.assertEqual(path, "/_matrix/key/v2/server")
- response = {
- "server_name": server_name,
- "old_verify_keys": {},
- "valid_until_ts": 200 * 1000,
- "verify_keys": {
- key_id: {
- "key": signedjson.key.encode_verify_key_base64(
- signedjson.key.get_verify_key(signing_key)
- )
- }
- },
- }
- sign_json(response, server_name, signing_key)
- return response
- self.http_client.get_json.side_effect = get_json
- class RemoteKeyResourceTestCase(BaseRemoteKeyResourceTestCase):
- def make_notary_request(self, server_name: str, key_id: str) -> dict:
- """Send a GET request to the test server requesting the given key.
- Checks that the response is a 200 and returns the decoded json body.
- """
- channel = FakeChannel(self.site, self.reactor)
- # channel is a `FakeChannel` but `HTTPChannel` is expected
- req = SynapseRequest(channel, self.site) # type: ignore[arg-type]
- req.content = BytesIO(b"")
- req.requestReceived(
- b"GET",
- b"/_matrix/key/v2/query/%s/%s"
- % (server_name.encode("utf-8"), key_id.encode("utf-8")),
- b"1.1",
- )
- channel.await_result()
- self.assertEqual(channel.code, 200)
- resp = channel.json_body
- return resp
- def test_get_key(self) -> None:
- """Fetch a remote key"""
- SERVER_NAME = "remote.server"
- testkey = signedjson.key.generate_signing_key("ver1")
- self.expect_outgoing_key_request(SERVER_NAME, testkey)
- resp = self.make_notary_request(SERVER_NAME, "ed25519:ver1")
- keys = resp["server_keys"]
- self.assertEqual(len(keys), 1)
- self.assertIn("ed25519:ver1", keys[0]["verify_keys"])
- self.assertEqual(len(keys[0]["verify_keys"]), 1)
- # it should be signed by both the origin server and the notary
- self.assertIn(SERVER_NAME, keys[0]["signatures"])
- self.assertIn(self.hs.hostname, keys[0]["signatures"])
- def test_get_own_key(self) -> None:
- """Fetch our own key"""
- testkey = signedjson.key.generate_signing_key("ver1")
- self.expect_outgoing_key_request(self.hs.hostname, testkey)
- resp = self.make_notary_request(self.hs.hostname, "ed25519:ver1")
- keys = resp["server_keys"]
- self.assertEqual(len(keys), 1)
- # it should be signed by both itself, and the notary signing key
- sigs = keys[0]["signatures"]
- self.assertEqual(len(sigs), 1)
- self.assertIn(self.hs.hostname, sigs)
- oursigs = sigs[self.hs.hostname]
- self.assertEqual(len(oursigs), 2)
- # the requested key should be present in the verify_keys section
- self.assertIn("ed25519:ver1", keys[0]["verify_keys"])
- class EndToEndPerspectivesTests(BaseRemoteKeyResourceTestCase):
- """End-to-end tests of the perspectives fetch case
- The idea here is to actually wire up a PerspectivesKeyFetcher to the notary
- endpoint, to check that the two implementations are compatible.
- """
- def default_config(self) -> Dict[str, Any]:
- config = super().default_config()
- # replace the signing key with our own
- self.hs_signing_key = signedjson.key.generate_signing_key("kssk")
- strm = StringIO()
- signedjson.key.write_signing_keys(strm, [self.hs_signing_key])
- config["signing_key"] = strm.getvalue()
- return config
- def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
- # make a second homeserver, configured to use the first one as a key notary
- self.http_client2 = Mock()
- config = default_config(name="keyclient")
- config["trusted_key_servers"] = [
- {
- "server_name": self.hs.hostname,
- "verify_keys": {
- "ed25519:%s"
- % (
- self.hs_signing_key.version,
- ): signedjson.key.encode_verify_key_base64(
- signedjson.key.get_verify_key(self.hs_signing_key)
- )
- },
- }
- ]
- self.hs2 = self.setup_test_homeserver(
- federation_http_client=self.http_client2, config=config
- )
- # wire up outbound POST /key/v2/query requests from hs2 so that they
- # will be forwarded to hs1
- async def post_json(
- destination: str, path: str, data: Optional[JsonDict] = None
- ) -> Union[JsonDict, list]:
- self.assertEqual(destination, self.hs.hostname)
- self.assertEqual(
- path,
- "/_matrix/key/v2/query",
- )
- channel = FakeChannel(self.site, self.reactor)
- # channel is a `FakeChannel` but `HTTPChannel` is expected
- req = SynapseRequest(channel, self.site) # type: ignore[arg-type]
- req.content = BytesIO(encode_canonical_json(data))
- req.requestReceived(
- b"POST",
- path.encode("utf-8"),
- b"1.1",
- )
- channel.await_result()
- self.assertEqual(channel.code, 200)
- resp = channel.json_body
- return resp
- self.http_client2.post_json.side_effect = post_json
- def test_get_key(self) -> None:
- """Fetch a key belonging to a random server"""
- # make up a key to be fetched.
- testkey = signedjson.key.generate_signing_key("abc")
- # we expect hs1 to make a regular key request to the target server
- self.expect_outgoing_key_request("targetserver", testkey)
- keyid = "ed25519:%s" % (testkey.version,)
- fetcher = PerspectivesKeyFetcher(self.hs2)
- d = fetcher.get_keys("targetserver", [keyid], 1000)
- res = self.get_success(d)
- self.assertIn(keyid, res)
- keyres = res[keyid]
- assert isinstance(keyres, FetchKeyResult)
- self.assertEqual(
- signedjson.key.encode_verify_key_base64(keyres.verify_key),
- signedjson.key.encode_verify_key_base64(
- signedjson.key.get_verify_key(testkey)
- ),
- )
- def test_get_notary_key(self) -> None:
- """Fetch a key belonging to the notary server"""
- # make up a key to be fetched. We randomise the keyid to try to get it to
- # appear before the key server signing key sometimes (otherwise we bail out
- # before fetching its signature)
- testkey = signedjson.key.generate_signing_key(random_string(5))
- # we expect hs1 to make a regular key request to itself
- self.expect_outgoing_key_request(self.hs.hostname, testkey)
- keyid = "ed25519:%s" % (testkey.version,)
- fetcher = PerspectivesKeyFetcher(self.hs2)
- d = fetcher.get_keys(self.hs.hostname, [keyid], 1000)
- res = self.get_success(d)
- self.assertIn(keyid, res)
- keyres = res[keyid]
- assert isinstance(keyres, FetchKeyResult)
- self.assertEqual(
- signedjson.key.encode_verify_key_base64(keyres.verify_key),
- signedjson.key.encode_verify_key_base64(
- signedjson.key.get_verify_key(testkey)
- ),
- )
- def test_get_notary_keyserver_key(self) -> None:
- """Fetch the notary's keyserver key"""
- # we expect hs1 to make a regular key request to itself
- self.expect_outgoing_key_request(self.hs.hostname, self.hs_signing_key)
- keyid = "ed25519:%s" % (self.hs_signing_key.version,)
- fetcher = PerspectivesKeyFetcher(self.hs2)
- d = fetcher.get_keys(self.hs.hostname, [keyid], 1000)
- res = self.get_success(d)
- self.assertIn(keyid, res)
- keyres = res[keyid]
- assert isinstance(keyres, FetchKeyResult)
- self.assertEqual(
- signedjson.key.encode_verify_key_base64(keyres.verify_key),
- signedjson.key.encode_verify_key_base64(
- signedjson.key.get_verify_key(self.hs_signing_key)
- ),
- )
|