You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

766 lines
27 KiB

  1. # Copyright 2019 New Vector Ltd
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. from typing import Callable, FrozenSet, List, Optional, Set
  15. from unittest.mock import AsyncMock, Mock
  16. from signedjson import key, sign
  17. from signedjson.types import BaseKey, SigningKey
  18. from twisted.internet import defer
  19. from twisted.test.proto_helpers import MemoryReactor
  20. from synapse.api.constants import EduTypes, RoomEncryptionAlgorithms
  21. from synapse.federation.units import Transaction
  22. from synapse.handlers.device import DeviceHandler
  23. from synapse.rest import admin
  24. from synapse.rest.client import login
  25. from synapse.server import HomeServer
  26. from synapse.types import JsonDict, ReadReceipt
  27. from synapse.util import Clock
  28. from tests.unittest import HomeserverTestCase
  29. class FederationSenderReceiptsTestCases(HomeserverTestCase):
  30. """
  31. Test federation sending to update receipts.
  32. By default for test cases federation sending is disabled. This Test class has it
  33. re-enabled for the main process.
  34. """
  35. def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
  36. self.federation_transport_client = Mock(spec=["send_transaction"])
  37. self.federation_transport_client.send_transaction = AsyncMock()
  38. hs = self.setup_test_homeserver(
  39. federation_transport_client=self.federation_transport_client,
  40. )
  41. hs.get_storage_controllers().state.get_current_hosts_in_room = AsyncMock( # type: ignore[method-assign]
  42. return_value={"test", "host2"}
  43. )
  44. hs.get_storage_controllers().state.get_current_hosts_in_room_or_partial_state_approximation = ( # type: ignore[method-assign]
  45. hs.get_storage_controllers().state.get_current_hosts_in_room
  46. )
  47. return hs
  48. def default_config(self) -> JsonDict:
  49. config = super().default_config()
  50. config["federation_sender_instances"] = None
  51. return config
  52. def test_send_receipts(self) -> None:
  53. mock_send_transaction = self.federation_transport_client.send_transaction
  54. mock_send_transaction.return_value = {}
  55. sender = self.hs.get_federation_sender()
  56. receipt = ReadReceipt(
  57. "room_id",
  58. "m.read",
  59. "user_id",
  60. ["event_id"],
  61. thread_id=None,
  62. data={"ts": 1234},
  63. )
  64. self.get_success(sender.send_read_receipt(receipt))
  65. self.pump()
  66. # expect a call to send_transaction
  67. mock_send_transaction.assert_called_once()
  68. json_cb = mock_send_transaction.call_args[0][1]
  69. data = json_cb()
  70. self.assertEqual(
  71. data["edus"],
  72. [
  73. {
  74. "edu_type": EduTypes.RECEIPT,
  75. "content": {
  76. "room_id": {
  77. "m.read": {
  78. "user_id": {
  79. "event_ids": ["event_id"],
  80. "data": {"ts": 1234},
  81. }
  82. }
  83. }
  84. },
  85. }
  86. ],
  87. )
  88. def test_send_receipts_thread(self) -> None:
  89. mock_send_transaction = self.federation_transport_client.send_transaction
  90. mock_send_transaction.return_value = {}
  91. # Create receipts for:
  92. #
  93. # * The same room / user on multiple threads.
  94. # * A different user in the same room.
  95. sender = self.hs.get_federation_sender()
  96. # Hack so that we have a txn in-flight so we batch up read receipts
  97. # below
  98. sender.wake_destination("host2")
  99. for user, thread in (
  100. ("alice", None),
  101. ("alice", "thread"),
  102. ("bob", None),
  103. ("bob", "diff-thread"),
  104. ):
  105. receipt = ReadReceipt(
  106. "room_id",
  107. "m.read",
  108. user,
  109. ["event_id"],
  110. thread_id=thread,
  111. data={"ts": 1234},
  112. )
  113. defer.ensureDeferred(sender.send_read_receipt(receipt))
  114. self.pump()
  115. # expect a call to send_transaction with two EDUs to separate threads.
  116. mock_send_transaction.assert_called_once()
  117. json_cb = mock_send_transaction.call_args[0][1]
  118. data = json_cb()
  119. # Note that the ordering of the EDUs doesn't matter.
  120. self.assertCountEqual(
  121. data["edus"],
  122. [
  123. {
  124. "edu_type": EduTypes.RECEIPT,
  125. "content": {
  126. "room_id": {
  127. "m.read": {
  128. "alice": {
  129. "event_ids": ["event_id"],
  130. "data": {"ts": 1234, "thread_id": "thread"},
  131. },
  132. "bob": {
  133. "event_ids": ["event_id"],
  134. "data": {"ts": 1234, "thread_id": "diff-thread"},
  135. },
  136. }
  137. }
  138. },
  139. },
  140. {
  141. "edu_type": EduTypes.RECEIPT,
  142. "content": {
  143. "room_id": {
  144. "m.read": {
  145. "alice": {
  146. "event_ids": ["event_id"],
  147. "data": {"ts": 1234},
  148. },
  149. "bob": {
  150. "event_ids": ["event_id"],
  151. "data": {"ts": 1234},
  152. },
  153. }
  154. }
  155. },
  156. },
  157. ],
  158. )
  159. def test_send_receipts_with_backoff(self) -> None:
  160. """Send two receipts in quick succession; the second should be flushed, but
  161. only after 20ms"""
  162. mock_send_transaction = self.federation_transport_client.send_transaction
  163. mock_send_transaction.return_value = {}
  164. sender = self.hs.get_federation_sender()
  165. receipt = ReadReceipt(
  166. "room_id",
  167. "m.read",
  168. "user_id",
  169. ["event_id"],
  170. thread_id=None,
  171. data={"ts": 1234},
  172. )
  173. self.get_success(sender.send_read_receipt(receipt))
  174. self.pump()
  175. # expect a call to send_transaction
  176. mock_send_transaction.assert_called_once()
  177. json_cb = mock_send_transaction.call_args[0][1]
  178. data = json_cb()
  179. self.assertEqual(
  180. data["edus"],
  181. [
  182. {
  183. "edu_type": EduTypes.RECEIPT,
  184. "content": {
  185. "room_id": {
  186. "m.read": {
  187. "user_id": {
  188. "event_ids": ["event_id"],
  189. "data": {"ts": 1234},
  190. }
  191. }
  192. }
  193. },
  194. }
  195. ],
  196. )
  197. mock_send_transaction.reset_mock()
  198. # send the second RR
  199. receipt = ReadReceipt(
  200. "room_id",
  201. "m.read",
  202. "user_id",
  203. ["other_id"],
  204. thread_id=None,
  205. data={"ts": 1234},
  206. )
  207. self.successResultOf(defer.ensureDeferred(sender.send_read_receipt(receipt)))
  208. self.pump()
  209. mock_send_transaction.assert_not_called()
  210. self.reactor.advance(19)
  211. mock_send_transaction.assert_not_called()
  212. self.reactor.advance(10)
  213. mock_send_transaction.assert_called_once()
  214. json_cb = mock_send_transaction.call_args[0][1]
  215. data = json_cb()
  216. self.assertEqual(
  217. data["edus"],
  218. [
  219. {
  220. "edu_type": EduTypes.RECEIPT,
  221. "content": {
  222. "room_id": {
  223. "m.read": {
  224. "user_id": {
  225. "event_ids": ["other_id"],
  226. "data": {"ts": 1234},
  227. }
  228. }
  229. }
  230. },
  231. }
  232. ],
  233. )
  234. class FederationSenderDevicesTestCases(HomeserverTestCase):
  235. """
  236. Test federation sending to update devices.
  237. By default for test cases federation sending is disabled. This Test class has it
  238. re-enabled for the main process.
  239. """
  240. servlets = [
  241. admin.register_servlets,
  242. login.register_servlets,
  243. ]
  244. def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
  245. self.federation_transport_client = Mock(
  246. spec=["send_transaction", "query_user_devices"]
  247. )
  248. self.federation_transport_client.send_transaction = AsyncMock()
  249. self.federation_transport_client.query_user_devices = AsyncMock()
  250. return self.setup_test_homeserver(
  251. federation_transport_client=self.federation_transport_client,
  252. )
  253. def default_config(self) -> JsonDict:
  254. c = super().default_config()
  255. # Enable federation sending on the main process.
  256. c["federation_sender_instances"] = None
  257. return c
  258. def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
  259. test_room_id = "!room:host1"
  260. # stub out `get_rooms_for_user` and `get_current_hosts_in_room` so that the
  261. # server thinks the user shares a room with `@user2:host2`
  262. def get_rooms_for_user(user_id: str) -> "defer.Deferred[FrozenSet[str]]":
  263. return defer.succeed(frozenset({test_room_id}))
  264. hs.get_datastores().main.get_rooms_for_user = get_rooms_for_user # type: ignore[assignment]
  265. async def get_current_hosts_in_room(room_id: str) -> Set[str]:
  266. if room_id == test_room_id:
  267. return {"host2"}
  268. else:
  269. # TODO: We should fail the test when we encounter an unxpected room ID.
  270. # We can't just use `self.fail(...)` here because the app code is greedy
  271. # with `Exception` and will catch it before the test can see it.
  272. return set()
  273. hs.get_datastores().main.get_current_hosts_in_room = get_current_hosts_in_room # type: ignore[assignment]
  274. device_handler = hs.get_device_handler()
  275. assert isinstance(device_handler, DeviceHandler)
  276. self.device_handler = device_handler
  277. # whenever send_transaction is called, record the edu data
  278. self.edus: List[JsonDict] = []
  279. self.federation_transport_client.send_transaction.side_effect = (
  280. self.record_transaction
  281. )
  282. async def record_transaction(
  283. self, txn: Transaction, json_cb: Optional[Callable[[], JsonDict]] = None
  284. ) -> JsonDict:
  285. assert json_cb is not None
  286. data = json_cb()
  287. self.edus.extend(data["edus"])
  288. return {}
  289. def test_send_device_updates(self) -> None:
  290. """Basic case: each device update should result in an EDU"""
  291. # create a device
  292. u1 = self.register_user("user", "pass")
  293. self.login(u1, "pass", device_id="D1")
  294. # expect one edu
  295. self.assertEqual(len(self.edus), 1)
  296. stream_id = self.check_device_update_edu(self.edus.pop(0), u1, "D1", None)
  297. # We queue up device list updates to be sent over federation, so we
  298. # advance to clear the queue.
  299. self.reactor.advance(1)
  300. # a second call should produce no new device EDUs
  301. self.get_success(
  302. self.hs.get_federation_sender().send_device_messages(["host2"])
  303. )
  304. self.assertEqual(self.edus, [])
  305. # a second device
  306. self.login("user", "pass", device_id="D2")
  307. self.assertEqual(len(self.edus), 1)
  308. self.check_device_update_edu(self.edus.pop(0), u1, "D2", stream_id)
  309. def test_dont_send_device_updates_for_remote_users(self) -> None:
  310. """Check that we don't send device updates for remote users"""
  311. # Send the server a device list EDU for the other user, this will cause
  312. # it to try and resync the device lists.
  313. self.federation_transport_client.query_user_devices.return_value = {
  314. "stream_id": "1",
  315. "user_id": "@user2:host2",
  316. "devices": [{"device_id": "D1"}],
  317. }
  318. self.get_success(
  319. self.device_handler.device_list_updater.incoming_device_list_update(
  320. "host2",
  321. {
  322. "user_id": "@user2:host2",
  323. "device_id": "D1",
  324. "stream_id": "1",
  325. "prev_ids": [],
  326. },
  327. )
  328. )
  329. self.reactor.advance(1)
  330. # We shouldn't see an EDU for that update
  331. self.assertEqual(self.edus, [])
  332. # Check that we did successfully process the inbound EDU (otherwise this
  333. # test would pass if we failed to process the EDU)
  334. devices = self.get_success(
  335. self.hs.get_datastores().main.get_cached_devices_for_user("@user2:host2")
  336. )
  337. self.assertIn("D1", devices)
  338. def test_upload_signatures(self) -> None:
  339. """Uploading signatures on some devices should produce updates for that user"""
  340. e2e_handler = self.hs.get_e2e_keys_handler()
  341. # register two devices
  342. u1 = self.register_user("user", "pass")
  343. self.login(u1, "pass", device_id="D1")
  344. self.login(u1, "pass", device_id="D2")
  345. # expect two edus
  346. self.assertEqual(len(self.edus), 2)
  347. stream_id: Optional[int] = None
  348. stream_id = self.check_device_update_edu(self.edus.pop(0), u1, "D1", stream_id)
  349. stream_id = self.check_device_update_edu(self.edus.pop(0), u1, "D2", stream_id)
  350. # upload signing keys for each device
  351. device1_signing_key = self.generate_and_upload_device_signing_key(u1, "D1")
  352. device2_signing_key = self.generate_and_upload_device_signing_key(u1, "D2")
  353. # We queue up device list updates to be sent over federation, so we
  354. # advance to clear the queue.
  355. self.reactor.advance(1)
  356. # expect two more edus
  357. self.assertEqual(len(self.edus), 2)
  358. stream_id = self.check_device_update_edu(self.edus.pop(0), u1, "D1", stream_id)
  359. stream_id = self.check_device_update_edu(self.edus.pop(0), u1, "D2", stream_id)
  360. # upload master key and self-signing key
  361. master_signing_key = generate_self_id_key()
  362. master_key = {
  363. "user_id": u1,
  364. "usage": ["master"],
  365. "keys": {key_id(master_signing_key): encode_pubkey(master_signing_key)},
  366. }
  367. # private key: HvQBbU+hc2Zr+JP1sE0XwBe1pfZZEYtJNPJLZJtS+F8
  368. selfsigning_signing_key = generate_self_id_key()
  369. selfsigning_key = {
  370. "user_id": u1,
  371. "usage": ["self_signing"],
  372. "keys": {
  373. key_id(selfsigning_signing_key): encode_pubkey(selfsigning_signing_key)
  374. },
  375. }
  376. sign.sign_json(selfsigning_key, u1, master_signing_key)
  377. cross_signing_keys = {
  378. "master_key": master_key,
  379. "self_signing_key": selfsigning_key,
  380. }
  381. self.get_success(
  382. e2e_handler.upload_signing_keys_for_user(u1, cross_signing_keys)
  383. )
  384. # We queue up device list updates to be sent over federation, so we
  385. # advance to clear the queue.
  386. self.reactor.advance(1)
  387. # expect signing key update edu
  388. self.assertEqual(len(self.edus), 2)
  389. self.assertEqual(self.edus.pop(0)["edu_type"], EduTypes.SIGNING_KEY_UPDATE)
  390. self.assertEqual(
  391. self.edus.pop(0)["edu_type"], EduTypes.UNSTABLE_SIGNING_KEY_UPDATE
  392. )
  393. # sign the devices
  394. d1_json = build_device_dict(u1, "D1", device1_signing_key)
  395. sign.sign_json(d1_json, u1, selfsigning_signing_key)
  396. d2_json = build_device_dict(u1, "D2", device2_signing_key)
  397. sign.sign_json(d2_json, u1, selfsigning_signing_key)
  398. ret = self.get_success(
  399. e2e_handler.upload_signatures_for_device_keys(
  400. u1,
  401. {u1: {"D1": d1_json, "D2": d2_json}},
  402. )
  403. )
  404. self.assertEqual(ret["failures"], {})
  405. # We queue up device list updates to be sent over federation, so we
  406. # advance to clear the queue.
  407. self.reactor.advance(1)
  408. # expect two edus, in one or two transactions. We don't know what order the
  409. # devices will be updated.
  410. self.assertEqual(len(self.edus), 2)
  411. stream_id = None # FIXME: there is a discontinuity in the stream IDs: see https://github.com/matrix-org/synapse/issues/7142
  412. for edu in self.edus:
  413. self.assertEqual(edu["edu_type"], EduTypes.DEVICE_LIST_UPDATE)
  414. c = edu["content"]
  415. if stream_id is not None:
  416. self.assertEqual(c["prev_id"], [stream_id]) # type: ignore[unreachable]
  417. self.assertGreaterEqual(c["stream_id"], stream_id)
  418. stream_id = c["stream_id"]
  419. devices = {edu["content"]["device_id"] for edu in self.edus}
  420. self.assertEqual({"D1", "D2"}, devices)
  421. def test_delete_devices(self) -> None:
  422. """If devices are deleted, that should result in EDUs too"""
  423. # create devices
  424. u1 = self.register_user("user", "pass")
  425. self.login("user", "pass", device_id="D1")
  426. self.login("user", "pass", device_id="D2")
  427. self.login("user", "pass", device_id="D3")
  428. # We queue up device list updates to be sent over federation, so we
  429. # advance to clear the queue.
  430. self.reactor.advance(1)
  431. # expect three edus
  432. self.assertEqual(len(self.edus), 3)
  433. stream_id = self.check_device_update_edu(self.edus.pop(0), u1, "D1", None)
  434. stream_id = self.check_device_update_edu(self.edus.pop(0), u1, "D2", stream_id)
  435. stream_id = self.check_device_update_edu(self.edus.pop(0), u1, "D3", stream_id)
  436. # delete them again
  437. self.get_success(self.device_handler.delete_devices(u1, ["D1", "D2", "D3"]))
  438. # We queue up device list updates to be sent over federation, so we
  439. # advance to clear the queue.
  440. self.reactor.advance(1)
  441. # expect three edus, in an unknown order
  442. self.assertEqual(len(self.edus), 3)
  443. for edu in self.edus:
  444. self.assertEqual(edu["edu_type"], EduTypes.DEVICE_LIST_UPDATE)
  445. c = edu["content"]
  446. self.assertGreaterEqual(
  447. c.items(),
  448. {"user_id": u1, "prev_id": [stream_id], "deleted": True}.items(),
  449. )
  450. self.assertGreaterEqual(c["stream_id"], stream_id)
  451. stream_id = c["stream_id"]
  452. devices = {edu["content"]["device_id"] for edu in self.edus}
  453. self.assertEqual({"D1", "D2", "D3"}, devices)
  454. def test_unreachable_server(self) -> None:
  455. """If the destination server is unreachable, all the updates should get sent on
  456. recovery
  457. """
  458. mock_send_txn = self.federation_transport_client.send_transaction
  459. mock_send_txn.side_effect = AssertionError("fail")
  460. # create devices
  461. u1 = self.register_user("user", "pass")
  462. self.login("user", "pass", device_id="D1")
  463. self.login("user", "pass", device_id="D2")
  464. self.login("user", "pass", device_id="D3")
  465. # delete them again
  466. self.get_success(self.device_handler.delete_devices(u1, ["D1", "D2", "D3"]))
  467. # We queue up device list updates to be sent over federation, so we
  468. # advance to clear the queue.
  469. self.reactor.advance(1)
  470. self.assertGreaterEqual(mock_send_txn.call_count, 4)
  471. # recover the server
  472. mock_send_txn.side_effect = self.record_transaction
  473. self.get_success(
  474. self.hs.get_federation_sender().send_device_messages(["host2"])
  475. )
  476. # We queue up device list updates to be sent over federation, so we
  477. # advance to clear the queue.
  478. self.reactor.advance(1)
  479. # for each device, there should be a single update
  480. self.assertEqual(len(self.edus), 3)
  481. stream_id: Optional[int] = None
  482. for edu in self.edus:
  483. self.assertEqual(edu["edu_type"], EduTypes.DEVICE_LIST_UPDATE)
  484. c = edu["content"]
  485. self.assertEqual(c["prev_id"], [stream_id] if stream_id is not None else [])
  486. if stream_id is not None:
  487. self.assertGreaterEqual(c["stream_id"], stream_id)
  488. stream_id = c["stream_id"]
  489. devices = {edu["content"]["device_id"] for edu in self.edus}
  490. self.assertEqual({"D1", "D2", "D3"}, devices)
  491. def test_prune_outbound_device_pokes1(self) -> None:
  492. """If a destination is unreachable, and the updates are pruned, we should get
  493. a single update.
  494. This case tests the behaviour when the server has never been reachable.
  495. """
  496. mock_send_txn = self.federation_transport_client.send_transaction
  497. mock_send_txn.side_effect = AssertionError("fail")
  498. # create devices
  499. u1 = self.register_user("user", "pass")
  500. self.login("user", "pass", device_id="D1")
  501. self.login("user", "pass", device_id="D2")
  502. self.login("user", "pass", device_id="D3")
  503. # delete them again
  504. self.get_success(self.device_handler.delete_devices(u1, ["D1", "D2", "D3"]))
  505. # We queue up device list updates to be sent over federation, so we
  506. # advance to clear the queue.
  507. self.reactor.advance(1)
  508. self.assertGreaterEqual(mock_send_txn.call_count, 4)
  509. # run the prune job
  510. self.reactor.advance(10)
  511. self.get_success(
  512. self.hs.get_datastores().main._prune_old_outbound_device_pokes(prune_age=1)
  513. )
  514. # recover the server
  515. mock_send_txn.side_effect = self.record_transaction
  516. self.get_success(
  517. self.hs.get_federation_sender().send_device_messages(["host2"])
  518. )
  519. # We queue up device list updates to be sent over federation, so we
  520. # advance to clear the queue.
  521. self.reactor.advance(1)
  522. # there should be a single update for this user.
  523. self.assertEqual(len(self.edus), 1)
  524. edu = self.edus.pop(0)
  525. self.assertEqual(edu["edu_type"], EduTypes.DEVICE_LIST_UPDATE)
  526. c = edu["content"]
  527. # synapse uses an empty prev_id list to indicate "needs a full resync".
  528. self.assertEqual(c["prev_id"], [])
  529. def test_prune_outbound_device_pokes2(self) -> None:
  530. """If a destination is unreachable, and the updates are pruned, we should get
  531. a single update.
  532. This case tests the behaviour when the server was reachable, but then goes
  533. offline.
  534. """
  535. # create first device
  536. u1 = self.register_user("user", "pass")
  537. self.login("user", "pass", device_id="D1")
  538. # expect the update EDU
  539. self.assertEqual(len(self.edus), 1)
  540. self.check_device_update_edu(self.edus.pop(0), u1, "D1", None)
  541. # now the server goes offline
  542. mock_send_txn = self.federation_transport_client.send_transaction
  543. mock_send_txn.side_effect = AssertionError("fail")
  544. self.login("user", "pass", device_id="D2")
  545. self.login("user", "pass", device_id="D3")
  546. # We queue up device list updates to be sent over federation, so we
  547. # advance to clear the queue.
  548. self.reactor.advance(1)
  549. # delete them again
  550. self.get_success(self.device_handler.delete_devices(u1, ["D1", "D2", "D3"]))
  551. self.assertGreaterEqual(mock_send_txn.call_count, 3)
  552. # run the prune job
  553. self.reactor.advance(10)
  554. self.get_success(
  555. self.hs.get_datastores().main._prune_old_outbound_device_pokes(prune_age=1)
  556. )
  557. # recover the server
  558. mock_send_txn.side_effect = self.record_transaction
  559. self.get_success(
  560. self.hs.get_federation_sender().send_device_messages(["host2"])
  561. )
  562. # We queue up device list updates to be sent over federation, so we
  563. # advance to clear the queue.
  564. self.reactor.advance(1)
  565. # ... and we should get a single update for this user.
  566. self.assertEqual(len(self.edus), 1)
  567. edu = self.edus.pop(0)
  568. self.assertEqual(edu["edu_type"], EduTypes.DEVICE_LIST_UPDATE)
  569. c = edu["content"]
  570. # synapse uses an empty prev_id list to indicate "needs a full resync".
  571. self.assertEqual(c["prev_id"], [])
  572. def check_device_update_edu(
  573. self,
  574. edu: JsonDict,
  575. user_id: str,
  576. device_id: str,
  577. prev_stream_id: Optional[int],
  578. ) -> int:
  579. """Check that the given EDU is an update for the given device
  580. Returns the stream_id.
  581. """
  582. self.assertEqual(edu["edu_type"], EduTypes.DEVICE_LIST_UPDATE)
  583. content = edu["content"]
  584. expected = {
  585. "user_id": user_id,
  586. "device_id": device_id,
  587. "prev_id": [prev_stream_id] if prev_stream_id is not None else [],
  588. }
  589. self.assertLessEqual(expected.items(), content.items())
  590. if prev_stream_id is not None:
  591. self.assertGreaterEqual(content["stream_id"], prev_stream_id)
  592. return content["stream_id"]
  593. def check_signing_key_update_txn(
  594. self,
  595. txn: JsonDict,
  596. ) -> None:
  597. """Check that the txn has an EDU with a signing key update."""
  598. edus = txn["edus"]
  599. self.assertEqual(len(edus), 2)
  600. def generate_and_upload_device_signing_key(
  601. self, user_id: str, device_id: str
  602. ) -> SigningKey:
  603. """Generate a signing keypair for the given device, and upload it"""
  604. sk = key.generate_signing_key(device_id)
  605. device_dict = build_device_dict(user_id, device_id, sk)
  606. self.get_success(
  607. self.hs.get_e2e_keys_handler().upload_keys_for_user(
  608. user_id,
  609. device_id,
  610. {"device_keys": device_dict},
  611. )
  612. )
  613. return sk
  614. def generate_self_id_key() -> SigningKey:
  615. """generate a signing key whose version is its public key
  616. ... as used by the cross-signing-keys.
  617. """
  618. k = key.generate_signing_key("x")
  619. k.version = encode_pubkey(k)
  620. return k
  621. def key_id(k: BaseKey) -> str:
  622. return "%s:%s" % (k.alg, k.version)
  623. def encode_pubkey(sk: SigningKey) -> str:
  624. """Encode the public key corresponding to the given signing key as base64"""
  625. return key.encode_verify_key_base64(key.get_verify_key(sk))
  626. def build_device_dict(user_id: str, device_id: str, sk: SigningKey) -> JsonDict:
  627. """Build a dict representing the given device"""
  628. return {
  629. "user_id": user_id,
  630. "device_id": device_id,
  631. "algorithms": [
  632. "m.olm.curve25519-aes-sha2",
  633. RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
  634. ],
  635. "keys": {
  636. "curve25519:" + device_id: "curve25519+key",
  637. key_id(sk): encode_pubkey(sk),
  638. },
  639. }