test_keyring.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2017 New Vector Ltd
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import time
  16. from mock import Mock
  17. import canonicaljson
  18. import signedjson.key
  19. import signedjson.sign
  20. from signedjson.key import encode_verify_key_base64, get_verify_key
  21. from twisted.internet import defer
  22. from synapse.api.errors import SynapseError
  23. from synapse.crypto import keyring
  24. from synapse.crypto.keyring import (
  25. PerspectivesKeyFetcher,
  26. ServerKeyFetcher,
  27. StoreKeyFetcher,
  28. )
  29. from synapse.logging.context import (
  30. LoggingContext,
  31. PreserveLoggingContext,
  32. make_deferred_yieldable,
  33. )
  34. from synapse.storage.keys import FetchKeyResult
  35. from tests import unittest
  36. class MockPerspectiveServer(object):
  37. def __init__(self):
  38. self.server_name = "mock_server"
  39. self.key = signedjson.key.generate_signing_key(0)
  40. def get_verify_keys(self):
  41. vk = signedjson.key.get_verify_key(self.key)
  42. return {"%s:%s" % (vk.alg, vk.version): encode_verify_key_base64(vk)}
  43. def get_signed_key(self, server_name, verify_key):
  44. key_id = "%s:%s" % (verify_key.alg, verify_key.version)
  45. res = {
  46. "server_name": server_name,
  47. "old_verify_keys": {},
  48. "valid_until_ts": time.time() * 1000 + 3600,
  49. "verify_keys": {key_id: {"key": encode_verify_key_base64(verify_key)}},
  50. }
  51. self.sign_response(res)
  52. return res
  53. def sign_response(self, res):
  54. signedjson.sign.sign_json(res, self.server_name, self.key)
  55. class KeyringTestCase(unittest.HomeserverTestCase):
  56. def make_homeserver(self, reactor, clock):
  57. self.mock_perspective_server = MockPerspectiveServer()
  58. self.http_client = Mock()
  59. config = self.default_config()
  60. config["trusted_key_servers"] = [
  61. {
  62. "server_name": self.mock_perspective_server.server_name,
  63. "verify_keys": self.mock_perspective_server.get_verify_keys(),
  64. }
  65. ]
  66. return self.setup_test_homeserver(
  67. handlers=None, http_client=self.http_client, config=config
  68. )
  69. def check_context(self, _, expected):
  70. self.assertEquals(
  71. getattr(LoggingContext.current_context(), "request", None), expected
  72. )
  73. def test_verify_json_objects_for_server_awaits_previous_requests(self):
  74. key1 = signedjson.key.generate_signing_key(1)
  75. kr = keyring.Keyring(self.hs)
  76. json1 = {}
  77. signedjson.sign.sign_json(json1, "server10", key1)
  78. persp_resp = {
  79. "server_keys": [
  80. self.mock_perspective_server.get_signed_key(
  81. "server10", signedjson.key.get_verify_key(key1)
  82. )
  83. ]
  84. }
  85. persp_deferred = defer.Deferred()
  86. @defer.inlineCallbacks
  87. def get_perspectives(**kwargs):
  88. self.assertEquals(LoggingContext.current_context().request, "11")
  89. with PreserveLoggingContext():
  90. yield persp_deferred
  91. return persp_resp
  92. self.http_client.post_json.side_effect = get_perspectives
  93. # start off a first set of lookups
  94. @defer.inlineCallbacks
  95. def first_lookup():
  96. with LoggingContext("11") as context_11:
  97. context_11.request = "11"
  98. res_deferreds = kr.verify_json_objects_for_server(
  99. [("server10", json1, 0, "test10"), ("server11", {}, 0, "test11")]
  100. )
  101. # the unsigned json should be rejected pretty quickly
  102. self.assertTrue(res_deferreds[1].called)
  103. try:
  104. yield res_deferreds[1]
  105. self.assertFalse("unsigned json didn't cause a failure")
  106. except SynapseError:
  107. pass
  108. self.assertFalse(res_deferreds[0].called)
  109. res_deferreds[0].addBoth(self.check_context, None)
  110. yield make_deferred_yieldable(res_deferreds[0])
  111. # let verify_json_objects_for_server finish its work before we kill the
  112. # logcontext
  113. yield self.clock.sleep(0)
  114. d0 = first_lookup()
  115. # wait a tick for it to send the request to the perspectives server
  116. # (it first tries the datastore)
  117. self.pump()
  118. self.http_client.post_json.assert_called_once()
  119. # a second request for a server with outstanding requests
  120. # should block rather than start a second call
  121. @defer.inlineCallbacks
  122. def second_lookup():
  123. with LoggingContext("12") as context_12:
  124. context_12.request = "12"
  125. self.http_client.post_json.reset_mock()
  126. self.http_client.post_json.return_value = defer.Deferred()
  127. res_deferreds_2 = kr.verify_json_objects_for_server(
  128. [("server10", json1, 0, "test")]
  129. )
  130. res_deferreds_2[0].addBoth(self.check_context, None)
  131. yield make_deferred_yieldable(res_deferreds_2[0])
  132. # let verify_json_objects_for_server finish its work before we kill the
  133. # logcontext
  134. yield self.clock.sleep(0)
  135. d2 = second_lookup()
  136. self.pump()
  137. self.http_client.post_json.assert_not_called()
  138. # complete the first request
  139. persp_deferred.callback(persp_resp)
  140. self.get_success(d0)
  141. self.get_success(d2)
  142. def test_verify_json_for_server(self):
  143. kr = keyring.Keyring(self.hs)
  144. key1 = signedjson.key.generate_signing_key(1)
  145. r = self.hs.datastore.store_server_verify_keys(
  146. "server9",
  147. time.time() * 1000,
  148. [("server9", get_key_id(key1), FetchKeyResult(get_verify_key(key1), 1000))],
  149. )
  150. self.get_success(r)
  151. json1 = {}
  152. signedjson.sign.sign_json(json1, "server9", key1)
  153. # should fail immediately on an unsigned object
  154. d = _verify_json_for_server(kr, "server9", {}, 0, "test unsigned")
  155. self.failureResultOf(d, SynapseError)
  156. # should suceed on a signed object
  157. d = _verify_json_for_server(kr, "server9", json1, 500, "test signed")
  158. # self.assertFalse(d.called)
  159. self.get_success(d)
  160. def test_verify_json_for_server_with_null_valid_until_ms(self):
  161. """Tests that we correctly handle key requests for keys we've stored
  162. with a null `ts_valid_until_ms`
  163. """
  164. mock_fetcher = keyring.KeyFetcher()
  165. mock_fetcher.get_keys = Mock(return_value=defer.succeed({}))
  166. kr = keyring.Keyring(
  167. self.hs, key_fetchers=(StoreKeyFetcher(self.hs), mock_fetcher)
  168. )
  169. key1 = signedjson.key.generate_signing_key(1)
  170. r = self.hs.datastore.store_server_verify_keys(
  171. "server9",
  172. time.time() * 1000,
  173. [("server9", get_key_id(key1), FetchKeyResult(get_verify_key(key1), None))],
  174. )
  175. self.get_success(r)
  176. json1 = {}
  177. signedjson.sign.sign_json(json1, "server9", key1)
  178. # should fail immediately on an unsigned object
  179. d = _verify_json_for_server(kr, "server9", {}, 0, "test unsigned")
  180. self.failureResultOf(d, SynapseError)
  181. # should fail on a signed object with a non-zero minimum_valid_until_ms,
  182. # as it tries to refetch the keys and fails.
  183. d = _verify_json_for_server(
  184. kr, "server9", json1, 500, "test signed non-zero min"
  185. )
  186. self.get_failure(d, SynapseError)
  187. # We expect the keyring tried to refetch the key once.
  188. mock_fetcher.get_keys.assert_called_once_with(
  189. {"server9": {get_key_id(key1): 500}}
  190. )
  191. # should succeed on a signed object with a 0 minimum_valid_until_ms
  192. d = _verify_json_for_server(
  193. kr, "server9", json1, 0, "test signed with zero min"
  194. )
  195. self.get_success(d)
  196. def test_verify_json_dedupes_key_requests(self):
  197. """Two requests for the same key should be deduped."""
  198. key1 = signedjson.key.generate_signing_key(1)
  199. def get_keys(keys_to_fetch):
  200. # there should only be one request object (with the max validity)
  201. self.assertEqual(keys_to_fetch, {"server1": {get_key_id(key1): 1500}})
  202. return defer.succeed(
  203. {
  204. "server1": {
  205. get_key_id(key1): FetchKeyResult(get_verify_key(key1), 1200)
  206. }
  207. }
  208. )
  209. mock_fetcher = keyring.KeyFetcher()
  210. mock_fetcher.get_keys = Mock(side_effect=get_keys)
  211. kr = keyring.Keyring(self.hs, key_fetchers=(mock_fetcher,))
  212. json1 = {}
  213. signedjson.sign.sign_json(json1, "server1", key1)
  214. # the first request should succeed; the second should fail because the key
  215. # has expired
  216. results = kr.verify_json_objects_for_server(
  217. [("server1", json1, 500, "test1"), ("server1", json1, 1500, "test2")]
  218. )
  219. self.assertEqual(len(results), 2)
  220. self.get_success(results[0])
  221. e = self.get_failure(results[1], SynapseError).value
  222. self.assertEqual(e.errcode, "M_UNAUTHORIZED")
  223. self.assertEqual(e.code, 401)
  224. # there should have been a single call to the fetcher
  225. mock_fetcher.get_keys.assert_called_once()
  226. def test_verify_json_falls_back_to_other_fetchers(self):
  227. """If the first fetcher cannot provide a recent enough key, we fall back"""
  228. key1 = signedjson.key.generate_signing_key(1)
  229. def get_keys1(keys_to_fetch):
  230. self.assertEqual(keys_to_fetch, {"server1": {get_key_id(key1): 1500}})
  231. return defer.succeed(
  232. {
  233. "server1": {
  234. get_key_id(key1): FetchKeyResult(get_verify_key(key1), 800)
  235. }
  236. }
  237. )
  238. def get_keys2(keys_to_fetch):
  239. self.assertEqual(keys_to_fetch, {"server1": {get_key_id(key1): 1500}})
  240. return defer.succeed(
  241. {
  242. "server1": {
  243. get_key_id(key1): FetchKeyResult(get_verify_key(key1), 1200)
  244. }
  245. }
  246. )
  247. mock_fetcher1 = keyring.KeyFetcher()
  248. mock_fetcher1.get_keys = Mock(side_effect=get_keys1)
  249. mock_fetcher2 = keyring.KeyFetcher()
  250. mock_fetcher2.get_keys = Mock(side_effect=get_keys2)
  251. kr = keyring.Keyring(self.hs, key_fetchers=(mock_fetcher1, mock_fetcher2))
  252. json1 = {}
  253. signedjson.sign.sign_json(json1, "server1", key1)
  254. results = kr.verify_json_objects_for_server(
  255. [("server1", json1, 1200, "test1"), ("server1", json1, 1500, "test2")]
  256. )
  257. self.assertEqual(len(results), 2)
  258. self.get_success(results[0])
  259. e = self.get_failure(results[1], SynapseError).value
  260. self.assertEqual(e.errcode, "M_UNAUTHORIZED")
  261. self.assertEqual(e.code, 401)
  262. # there should have been a single call to each fetcher
  263. mock_fetcher1.get_keys.assert_called_once()
  264. mock_fetcher2.get_keys.assert_called_once()
  265. class ServerKeyFetcherTestCase(unittest.HomeserverTestCase):
  266. def make_homeserver(self, reactor, clock):
  267. self.http_client = Mock()
  268. hs = self.setup_test_homeserver(handlers=None, http_client=self.http_client)
  269. return hs
  270. def test_get_keys_from_server(self):
  271. # arbitrarily advance the clock a bit
  272. self.reactor.advance(100)
  273. SERVER_NAME = "server2"
  274. fetcher = ServerKeyFetcher(self.hs)
  275. testkey = signedjson.key.generate_signing_key("ver1")
  276. testverifykey = signedjson.key.get_verify_key(testkey)
  277. testverifykey_id = "ed25519:ver1"
  278. VALID_UNTIL_TS = 200 * 1000
  279. # valid response
  280. response = {
  281. "server_name": SERVER_NAME,
  282. "old_verify_keys": {},
  283. "valid_until_ts": VALID_UNTIL_TS,
  284. "verify_keys": {
  285. testverifykey_id: {
  286. "key": signedjson.key.encode_verify_key_base64(testverifykey)
  287. }
  288. },
  289. }
  290. signedjson.sign.sign_json(response, SERVER_NAME, testkey)
  291. def get_json(destination, path, **kwargs):
  292. self.assertEqual(destination, SERVER_NAME)
  293. self.assertEqual(path, "/_matrix/key/v2/server/key1")
  294. return response
  295. self.http_client.get_json.side_effect = get_json
  296. keys_to_fetch = {SERVER_NAME: {"key1": 0}}
  297. keys = self.get_success(fetcher.get_keys(keys_to_fetch))
  298. k = keys[SERVER_NAME][testverifykey_id]
  299. self.assertEqual(k.valid_until_ts, VALID_UNTIL_TS)
  300. self.assertEqual(k.verify_key, testverifykey)
  301. self.assertEqual(k.verify_key.alg, "ed25519")
  302. self.assertEqual(k.verify_key.version, "ver1")
  303. # check that the perspectives store is correctly updated
  304. lookup_triplet = (SERVER_NAME, testverifykey_id, None)
  305. key_json = self.get_success(
  306. self.hs.get_datastore().get_server_keys_json([lookup_triplet])
  307. )
  308. res = key_json[lookup_triplet]
  309. self.assertEqual(len(res), 1)
  310. res = res[0]
  311. self.assertEqual(res["key_id"], testverifykey_id)
  312. self.assertEqual(res["from_server"], SERVER_NAME)
  313. self.assertEqual(res["ts_added_ms"], self.reactor.seconds() * 1000)
  314. self.assertEqual(res["ts_valid_until_ms"], VALID_UNTIL_TS)
  315. # we expect it to be encoded as canonical json *before* it hits the db
  316. self.assertEqual(
  317. bytes(res["key_json"]), canonicaljson.encode_canonical_json(response)
  318. )
  319. # change the server name: the result should be ignored
  320. response["server_name"] = "OTHER_SERVER"
  321. keys = self.get_success(fetcher.get_keys(keys_to_fetch))
  322. self.assertEqual(keys, {})
  323. class PerspectivesKeyFetcherTestCase(unittest.HomeserverTestCase):
  324. def make_homeserver(self, reactor, clock):
  325. self.mock_perspective_server = MockPerspectiveServer()
  326. self.http_client = Mock()
  327. config = self.default_config()
  328. config["trusted_key_servers"] = [
  329. {
  330. "server_name": self.mock_perspective_server.server_name,
  331. "verify_keys": self.mock_perspective_server.get_verify_keys(),
  332. }
  333. ]
  334. return self.setup_test_homeserver(
  335. handlers=None, http_client=self.http_client, config=config
  336. )
  337. def test_get_keys_from_perspectives(self):
  338. # arbitrarily advance the clock a bit
  339. self.reactor.advance(100)
  340. fetcher = PerspectivesKeyFetcher(self.hs)
  341. SERVER_NAME = "server2"
  342. testkey = signedjson.key.generate_signing_key("ver1")
  343. testverifykey = signedjson.key.get_verify_key(testkey)
  344. testverifykey_id = "ed25519:ver1"
  345. VALID_UNTIL_TS = 200 * 1000
  346. # valid response
  347. response = {
  348. "server_name": SERVER_NAME,
  349. "old_verify_keys": {},
  350. "valid_until_ts": VALID_UNTIL_TS,
  351. "verify_keys": {
  352. testverifykey_id: {
  353. "key": signedjson.key.encode_verify_key_base64(testverifykey)
  354. }
  355. },
  356. }
  357. # the response must be signed by both the origin server and the perspectives
  358. # server.
  359. signedjson.sign.sign_json(response, SERVER_NAME, testkey)
  360. self.mock_perspective_server.sign_response(response)
  361. def post_json(destination, path, data, **kwargs):
  362. self.assertEqual(destination, self.mock_perspective_server.server_name)
  363. self.assertEqual(path, "/_matrix/key/v2/query")
  364. # check that the request is for the expected key
  365. q = data["server_keys"]
  366. self.assertEqual(list(q[SERVER_NAME].keys()), ["key1"])
  367. return {"server_keys": [response]}
  368. self.http_client.post_json.side_effect = post_json
  369. keys_to_fetch = {SERVER_NAME: {"key1": 0}}
  370. keys = self.get_success(fetcher.get_keys(keys_to_fetch))
  371. self.assertIn(SERVER_NAME, keys)
  372. k = keys[SERVER_NAME][testverifykey_id]
  373. self.assertEqual(k.valid_until_ts, VALID_UNTIL_TS)
  374. self.assertEqual(k.verify_key, testverifykey)
  375. self.assertEqual(k.verify_key.alg, "ed25519")
  376. self.assertEqual(k.verify_key.version, "ver1")
  377. # check that the perspectives store is correctly updated
  378. lookup_triplet = (SERVER_NAME, testverifykey_id, None)
  379. key_json = self.get_success(
  380. self.hs.get_datastore().get_server_keys_json([lookup_triplet])
  381. )
  382. res = key_json[lookup_triplet]
  383. self.assertEqual(len(res), 1)
  384. res = res[0]
  385. self.assertEqual(res["key_id"], testverifykey_id)
  386. self.assertEqual(res["from_server"], self.mock_perspective_server.server_name)
  387. self.assertEqual(res["ts_added_ms"], self.reactor.seconds() * 1000)
  388. self.assertEqual(res["ts_valid_until_ms"], VALID_UNTIL_TS)
  389. self.assertEqual(
  390. bytes(res["key_json"]), canonicaljson.encode_canonical_json(response)
  391. )
  392. def test_invalid_perspectives_responses(self):
  393. """Check that invalid responses from the perspectives server are rejected"""
  394. # arbitrarily advance the clock a bit
  395. self.reactor.advance(100)
  396. SERVER_NAME = "server2"
  397. testkey = signedjson.key.generate_signing_key("ver1")
  398. testverifykey = signedjson.key.get_verify_key(testkey)
  399. testverifykey_id = "ed25519:ver1"
  400. VALID_UNTIL_TS = 200 * 1000
  401. def build_response():
  402. # valid response
  403. response = {
  404. "server_name": SERVER_NAME,
  405. "old_verify_keys": {},
  406. "valid_until_ts": VALID_UNTIL_TS,
  407. "verify_keys": {
  408. testverifykey_id: {
  409. "key": signedjson.key.encode_verify_key_base64(testverifykey)
  410. }
  411. },
  412. }
  413. # the response must be signed by both the origin server and the perspectives
  414. # server.
  415. signedjson.sign.sign_json(response, SERVER_NAME, testkey)
  416. self.mock_perspective_server.sign_response(response)
  417. return response
  418. def get_key_from_perspectives(response):
  419. fetcher = PerspectivesKeyFetcher(self.hs)
  420. keys_to_fetch = {SERVER_NAME: {"key1": 0}}
  421. def post_json(destination, path, data, **kwargs):
  422. self.assertEqual(destination, self.mock_perspective_server.server_name)
  423. self.assertEqual(path, "/_matrix/key/v2/query")
  424. return {"server_keys": [response]}
  425. self.http_client.post_json.side_effect = post_json
  426. return self.get_success(fetcher.get_keys(keys_to_fetch))
  427. # start with a valid response so we can check we are testing the right thing
  428. response = build_response()
  429. keys = get_key_from_perspectives(response)
  430. k = keys[SERVER_NAME][testverifykey_id]
  431. self.assertEqual(k.verify_key, testverifykey)
  432. # remove the perspectives server's signature
  433. response = build_response()
  434. del response["signatures"][self.mock_perspective_server.server_name]
  435. self.http_client.post_json.return_value = {"server_keys": [response]}
  436. keys = get_key_from_perspectives(response)
  437. self.assertEqual(keys, {}, "Expected empty dict with missing persp server sig")
  438. # remove the origin server's signature
  439. response = build_response()
  440. del response["signatures"][SERVER_NAME]
  441. self.http_client.post_json.return_value = {"server_keys": [response]}
  442. keys = get_key_from_perspectives(response)
  443. self.assertEqual(keys, {}, "Expected empty dict with missing origin server sig")
  444. def get_key_id(key):
  445. """Get the matrix ID tag for a given SigningKey or VerifyKey"""
  446. return "%s:%s" % (key.alg, key.version)
  447. @defer.inlineCallbacks
  448. def run_in_context(f, *args, **kwargs):
  449. with LoggingContext("testctx") as ctx:
  450. # we set the "request" prop to make it easier to follow what's going on in the
  451. # logs.
  452. ctx.request = "testctx"
  453. rv = yield f(*args, **kwargs)
  454. return rv
  455. def _verify_json_for_server(kr, *args):
  456. """thin wrapper around verify_json_for_server which makes sure it is wrapped
  457. with the patched defer.inlineCallbacks.
  458. """
  459. @defer.inlineCallbacks
  460. def v():
  461. rv1 = yield kr.verify_json_for_server(*args)
  462. return rv1
  463. return run_in_context(v)