279 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			279 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
| # 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.
 | |
| import urllib.parse
 | |
| 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 KeyApiV2Resource
 | |
| 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": KeyApiV2Resource(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/%s" % (urllib.parse.quote(key_id),)
 | |
|             )
 | |
| 
 | |
|             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)
 | |
|             ),
 | |
|         )
 |