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.
 
 
 
 
 
 

1652 lines
62 KiB

  1. # Copyright 2016 OpenMarket Ltd
  2. # Copyright 2019 New Vector Ltd
  3. # Copyright 2019 The Matrix.org Foundation C.I.C.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. from typing import Dict, Iterable
  17. from unittest import mock
  18. from parameterized import parameterized
  19. from signedjson import key as key, sign as sign
  20. from twisted.test.proto_helpers import MemoryReactor
  21. from synapse.api.constants import RoomEncryptionAlgorithms
  22. from synapse.api.errors import Codes, SynapseError
  23. from synapse.appservice import ApplicationService
  24. from synapse.handlers.device import DeviceHandler
  25. from synapse.server import HomeServer
  26. from synapse.storage.databases.main.appservice import _make_exclusive_regex
  27. from synapse.types import JsonDict, UserID
  28. from synapse.util import Clock
  29. from tests import unittest
  30. from tests.unittest import override_config
  31. class E2eKeysHandlerTestCase(unittest.HomeserverTestCase):
  32. def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
  33. self.appservice_api = mock.AsyncMock()
  34. return self.setup_test_homeserver(
  35. federation_client=mock.Mock(), application_service_api=self.appservice_api
  36. )
  37. def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
  38. self.handler = hs.get_e2e_keys_handler()
  39. self.store = self.hs.get_datastores().main
  40. self.requester = UserID.from_string(f"@test_requester:{self.hs.hostname}")
  41. def test_query_local_devices_no_devices(self) -> None:
  42. """If the user has no devices, we expect an empty list."""
  43. local_user = "@boris:" + self.hs.hostname
  44. res = self.get_success(self.handler.query_local_devices({local_user: None}))
  45. self.assertDictEqual(res, {local_user: {}})
  46. def test_reupload_one_time_keys(self) -> None:
  47. """we should be able to re-upload the same keys"""
  48. local_user = "@boris:" + self.hs.hostname
  49. device_id = "xyz"
  50. keys: JsonDict = {
  51. "alg1:k1": "key1",
  52. "alg2:k2": {"key": "key2", "signatures": {"k1": "sig1"}},
  53. "alg2:k3": {"key": "key3"},
  54. }
  55. # Note that "signed_curve25519" is always returned in key count responses. This is necessary until
  56. # https://github.com/matrix-org/matrix-doc/issues/3298 is fixed.
  57. res = self.get_success(
  58. self.handler.upload_keys_for_user(
  59. local_user, device_id, {"one_time_keys": keys}
  60. )
  61. )
  62. self.assertDictEqual(
  63. res, {"one_time_key_counts": {"alg1": 1, "alg2": 2, "signed_curve25519": 0}}
  64. )
  65. # we should be able to change the signature without a problem
  66. keys["alg2:k2"]["signatures"]["k1"] = "sig2"
  67. res = self.get_success(
  68. self.handler.upload_keys_for_user(
  69. local_user, device_id, {"one_time_keys": keys}
  70. )
  71. )
  72. self.assertDictEqual(
  73. res, {"one_time_key_counts": {"alg1": 1, "alg2": 2, "signed_curve25519": 0}}
  74. )
  75. def test_change_one_time_keys(self) -> None:
  76. """attempts to change one-time-keys should be rejected"""
  77. local_user = "@boris:" + self.hs.hostname
  78. device_id = "xyz"
  79. keys = {
  80. "alg1:k1": "key1",
  81. "alg2:k2": {"key": "key2", "signatures": {"k1": "sig1"}},
  82. "alg2:k3": {"key": "key3"},
  83. }
  84. res = self.get_success(
  85. self.handler.upload_keys_for_user(
  86. local_user, device_id, {"one_time_keys": keys}
  87. )
  88. )
  89. self.assertDictEqual(
  90. res, {"one_time_key_counts": {"alg1": 1, "alg2": 2, "signed_curve25519": 0}}
  91. )
  92. # Error when changing string key
  93. self.get_failure(
  94. self.handler.upload_keys_for_user(
  95. local_user, device_id, {"one_time_keys": {"alg1:k1": "key2"}}
  96. ),
  97. SynapseError,
  98. )
  99. # Error when replacing dict key with string
  100. self.get_failure(
  101. self.handler.upload_keys_for_user(
  102. local_user, device_id, {"one_time_keys": {"alg2:k3": "key2"}}
  103. ),
  104. SynapseError,
  105. )
  106. # Error when replacing string key with dict
  107. self.get_failure(
  108. self.handler.upload_keys_for_user(
  109. local_user,
  110. device_id,
  111. {"one_time_keys": {"alg1:k1": {"key": "key"}}},
  112. ),
  113. SynapseError,
  114. )
  115. # Error when replacing dict key
  116. self.get_failure(
  117. self.handler.upload_keys_for_user(
  118. local_user,
  119. device_id,
  120. {
  121. "one_time_keys": {
  122. "alg2:k2": {"key": "key3", "signatures": {"k1": "sig1"}}
  123. }
  124. },
  125. ),
  126. SynapseError,
  127. )
  128. def test_claim_one_time_key(self) -> None:
  129. local_user = "@boris:" + self.hs.hostname
  130. device_id = "xyz"
  131. keys = {"alg1:k1": "key1"}
  132. res = self.get_success(
  133. self.handler.upload_keys_for_user(
  134. local_user, device_id, {"one_time_keys": keys}
  135. )
  136. )
  137. self.assertDictEqual(
  138. res, {"one_time_key_counts": {"alg1": 1, "signed_curve25519": 0}}
  139. )
  140. res2 = self.get_success(
  141. self.handler.claim_one_time_keys(
  142. {local_user: {device_id: {"alg1": 1}}},
  143. self.requester,
  144. timeout=None,
  145. always_include_fallback_keys=False,
  146. )
  147. )
  148. self.assertEqual(
  149. res2,
  150. {
  151. "failures": {},
  152. "one_time_keys": {local_user: {device_id: {"alg1:k1": "key1"}}},
  153. },
  154. )
  155. def test_claim_one_time_key_bulk(self) -> None:
  156. """Like test_claim_one_time_key but claims multiple keys in one handler call."""
  157. # Apologies to the reader. This test is a little too verbose. It is particularly
  158. # tricky to make assertions neatly with all these nested dictionaries in play.
  159. # Three users with two devices each. Each device uses two algorithms.
  160. # Each algorithm is invoked with two keys.
  161. alice = f"@alice:{self.hs.hostname}"
  162. brian = f"@brian:{self.hs.hostname}"
  163. chris = f"@chris:{self.hs.hostname}"
  164. one_time_keys = {
  165. alice: {
  166. "alice_dev_1": {
  167. "alg1:k1": {"dummy_id": 1},
  168. "alg1:k2": {"dummy_id": 2},
  169. "alg2:k3": {"dummy_id": 3},
  170. "alg2:k4": {"dummy_id": 4},
  171. },
  172. "alice_dev_2": {
  173. "alg1:k5": {"dummy_id": 5},
  174. "alg1:k6": {"dummy_id": 6},
  175. "alg2:k7": {"dummy_id": 7},
  176. "alg2:k8": {"dummy_id": 8},
  177. },
  178. },
  179. brian: {
  180. "brian_dev_1": {
  181. "alg1:k9": {"dummy_id": 9},
  182. "alg1:k10": {"dummy_id": 10},
  183. "alg2:k11": {"dummy_id": 11},
  184. "alg2:k12": {"dummy_id": 12},
  185. },
  186. "brian_dev_2": {
  187. "alg1:k13": {"dummy_id": 13},
  188. "alg1:k14": {"dummy_id": 14},
  189. "alg2:k15": {"dummy_id": 15},
  190. "alg2:k16": {"dummy_id": 16},
  191. },
  192. },
  193. chris: {
  194. "chris_dev_1": {
  195. "alg1:k17": {"dummy_id": 17},
  196. "alg1:k18": {"dummy_id": 18},
  197. "alg2:k19": {"dummy_id": 19},
  198. "alg2:k20": {"dummy_id": 20},
  199. },
  200. "chris_dev_2": {
  201. "alg1:k21": {"dummy_id": 21},
  202. "alg1:k22": {"dummy_id": 22},
  203. "alg2:k23": {"dummy_id": 23},
  204. "alg2:k24": {"dummy_id": 24},
  205. },
  206. },
  207. }
  208. for user_id, devices in one_time_keys.items():
  209. for device_id, keys_dict in devices.items():
  210. counts = self.get_success(
  211. self.handler.upload_keys_for_user(
  212. user_id,
  213. device_id,
  214. {"one_time_keys": keys_dict},
  215. )
  216. )
  217. # The upload should report 2 keys per algorithm.
  218. expected_counts = {
  219. "one_time_key_counts": {
  220. # See count_e2e_one_time_keys for why this is hardcoded.
  221. "signed_curve25519": 0,
  222. "alg1": 2,
  223. "alg2": 2,
  224. },
  225. }
  226. self.assertEqual(counts, expected_counts)
  227. # Claim a variety of keys.
  228. # Raw format, easier to make test assertions about.
  229. claims_to_make = {
  230. (alice, "alice_dev_1", "alg1"): 1,
  231. (alice, "alice_dev_1", "alg2"): 2,
  232. (alice, "alice_dev_2", "alg2"): 1,
  233. (brian, "brian_dev_1", "alg1"): 2,
  234. (brian, "brian_dev_2", "alg2"): 9001,
  235. (chris, "chris_dev_2", "alg2"): 1,
  236. }
  237. # Convert to the format the handler wants.
  238. query: Dict[str, Dict[str, Dict[str, int]]] = {}
  239. for (user_id, device_id, algorithm), count in claims_to_make.items():
  240. query.setdefault(user_id, {}).setdefault(device_id, {})[algorithm] = count
  241. claim_res = self.get_success(
  242. self.handler.claim_one_time_keys(
  243. query,
  244. self.requester,
  245. timeout=None,
  246. always_include_fallback_keys=False,
  247. )
  248. )
  249. # No failures, please!
  250. self.assertEqual(claim_res["failures"], {})
  251. # Check that we get exactly the (user, device, algorithm)s we asked for.
  252. got_otks = claim_res["one_time_keys"]
  253. claimed_user_device_algorithms = {
  254. (user_id, device_id, alg_key_id.split(":")[0])
  255. for user_id, devices in got_otks.items()
  256. for device_id, key_dict in devices.items()
  257. for alg_key_id in key_dict
  258. }
  259. self.assertEqual(claimed_user_device_algorithms, set(claims_to_make))
  260. # Now check the keys we got are what we expected.
  261. def assertExactlyOneOtk(
  262. user_id: str, device_id: str, *alg_key_pairs: str
  263. ) -> None:
  264. key_dict = got_otks[user_id][device_id]
  265. found = 0
  266. for alg_key in alg_key_pairs:
  267. if alg_key in key_dict:
  268. expected_key_json = one_time_keys[user_id][device_id][alg_key]
  269. self.assertEqual(key_dict[alg_key], expected_key_json)
  270. found += 1
  271. self.assertEqual(found, 1)
  272. def assertAllOtks(user_id: str, device_id: str, *alg_key_pairs: str) -> None:
  273. key_dict = got_otks[user_id][device_id]
  274. for alg_key in alg_key_pairs:
  275. expected_key_json = one_time_keys[user_id][device_id][alg_key]
  276. self.assertEqual(key_dict[alg_key], expected_key_json)
  277. # Expect a single arbitrary key to be returned.
  278. assertExactlyOneOtk(alice, "alice_dev_1", "alg1:k1", "alg1:k2")
  279. assertExactlyOneOtk(alice, "alice_dev_2", "alg2:k7", "alg2:k8")
  280. assertExactlyOneOtk(chris, "chris_dev_2", "alg2:k23", "alg2:k24")
  281. assertAllOtks(alice, "alice_dev_1", "alg2:k3", "alg2:k4")
  282. assertAllOtks(brian, "brian_dev_1", "alg1:k9", "alg1:k10")
  283. assertAllOtks(brian, "brian_dev_2", "alg2:k15", "alg2:k16")
  284. # Now check the unused key counts.
  285. for user_id, devices in one_time_keys.items():
  286. for device_id in devices:
  287. counts_by_alg = self.get_success(
  288. self.store.count_e2e_one_time_keys(user_id, device_id)
  289. )
  290. # Somewhat fiddley to compute the expected count dict.
  291. expected_counts_by_alg = {
  292. "signed_curve25519": 0,
  293. }
  294. for alg in ["alg1", "alg2"]:
  295. claim_count = claims_to_make.get((user_id, device_id, alg), 0)
  296. remaining_count = max(0, 2 - claim_count)
  297. if remaining_count > 0:
  298. expected_counts_by_alg[alg] = remaining_count
  299. self.assertEqual(
  300. counts_by_alg, expected_counts_by_alg, f"{user_id}:{device_id}"
  301. )
  302. def test_fallback_key(self) -> None:
  303. local_user = "@boris:" + self.hs.hostname
  304. device_id = "xyz"
  305. fallback_key = {"alg1:k1": "fallback_key1"}
  306. fallback_key2 = {"alg1:k2": "fallback_key2"}
  307. fallback_key3 = {"alg1:k2": "fallback_key3"}
  308. otk = {"alg1:k2": "key2"}
  309. # we shouldn't have any unused fallback keys yet
  310. res = self.get_success(
  311. self.store.get_e2e_unused_fallback_key_types(local_user, device_id)
  312. )
  313. self.assertEqual(res, [])
  314. self.get_success(
  315. self.handler.upload_keys_for_user(
  316. local_user,
  317. device_id,
  318. {"fallback_keys": fallback_key},
  319. )
  320. )
  321. # we should now have an unused alg1 key
  322. fallback_res = self.get_success(
  323. self.store.get_e2e_unused_fallback_key_types(local_user, device_id)
  324. )
  325. self.assertEqual(fallback_res, ["alg1"])
  326. # claiming an OTK when no OTKs are available should return the fallback
  327. # key
  328. claim_res = self.get_success(
  329. self.handler.claim_one_time_keys(
  330. {local_user: {device_id: {"alg1": 1}}},
  331. self.requester,
  332. timeout=None,
  333. always_include_fallback_keys=False,
  334. )
  335. )
  336. self.assertEqual(
  337. claim_res,
  338. {"failures": {}, "one_time_keys": {local_user: {device_id: fallback_key}}},
  339. )
  340. # we shouldn't have any unused fallback keys again
  341. unused_res = self.get_success(
  342. self.store.get_e2e_unused_fallback_key_types(local_user, device_id)
  343. )
  344. self.assertEqual(unused_res, [])
  345. # claiming an OTK again should return the same fallback key
  346. claim_res = self.get_success(
  347. self.handler.claim_one_time_keys(
  348. {local_user: {device_id: {"alg1": 1}}},
  349. self.requester,
  350. timeout=None,
  351. always_include_fallback_keys=False,
  352. )
  353. )
  354. self.assertEqual(
  355. claim_res,
  356. {"failures": {}, "one_time_keys": {local_user: {device_id: fallback_key}}},
  357. )
  358. # re-uploading the same fallback key should still result in no unused fallback
  359. # keys
  360. self.get_success(
  361. self.handler.upload_keys_for_user(
  362. local_user,
  363. device_id,
  364. {"fallback_keys": fallback_key},
  365. )
  366. )
  367. unused_res = self.get_success(
  368. self.store.get_e2e_unused_fallback_key_types(local_user, device_id)
  369. )
  370. self.assertEqual(unused_res, [])
  371. # uploading a new fallback key should result in an unused fallback key
  372. self.get_success(
  373. self.handler.upload_keys_for_user(
  374. local_user,
  375. device_id,
  376. {"fallback_keys": fallback_key2},
  377. )
  378. )
  379. unused_res = self.get_success(
  380. self.store.get_e2e_unused_fallback_key_types(local_user, device_id)
  381. )
  382. self.assertEqual(unused_res, ["alg1"])
  383. # if the user uploads a one-time key, the next claim should fetch the
  384. # one-time key, and then go back to the fallback
  385. self.get_success(
  386. self.handler.upload_keys_for_user(
  387. local_user, device_id, {"one_time_keys": otk}
  388. )
  389. )
  390. claim_res = self.get_success(
  391. self.handler.claim_one_time_keys(
  392. {local_user: {device_id: {"alg1": 1}}},
  393. self.requester,
  394. timeout=None,
  395. always_include_fallback_keys=False,
  396. )
  397. )
  398. self.assertEqual(
  399. claim_res,
  400. {"failures": {}, "one_time_keys": {local_user: {device_id: otk}}},
  401. )
  402. claim_res = self.get_success(
  403. self.handler.claim_one_time_keys(
  404. {local_user: {device_id: {"alg1": 1}}},
  405. self.requester,
  406. timeout=None,
  407. always_include_fallback_keys=False,
  408. )
  409. )
  410. self.assertEqual(
  411. claim_res,
  412. {"failures": {}, "one_time_keys": {local_user: {device_id: fallback_key2}}},
  413. )
  414. # using the unstable prefix should also set the fallback key
  415. self.get_success(
  416. self.handler.upload_keys_for_user(
  417. local_user,
  418. device_id,
  419. {"org.matrix.msc2732.fallback_keys": fallback_key3},
  420. )
  421. )
  422. claim_res = self.get_success(
  423. self.handler.claim_one_time_keys(
  424. {local_user: {device_id: {"alg1": 1}}},
  425. self.requester,
  426. timeout=None,
  427. always_include_fallback_keys=False,
  428. )
  429. )
  430. self.assertEqual(
  431. claim_res,
  432. {"failures": {}, "one_time_keys": {local_user: {device_id: fallback_key3}}},
  433. )
  434. def test_fallback_key_bulk(self) -> None:
  435. """Like test_fallback_key, but claims multiple keys in one handler call."""
  436. alice = f"@alice:{self.hs.hostname}"
  437. brian = f"@brian:{self.hs.hostname}"
  438. chris = f"@chris:{self.hs.hostname}"
  439. # Have three users upload fallback keys for two devices.
  440. fallback_keys = {
  441. alice: {
  442. "alice_dev_1": {"alg1:k1": "fallback_key1"},
  443. "alice_dev_2": {"alg2:k2": "fallback_key2"},
  444. },
  445. brian: {
  446. "brian_dev_1": {"alg1:k3": "fallback_key3"},
  447. "brian_dev_2": {"alg2:k4": "fallback_key4"},
  448. },
  449. chris: {
  450. "chris_dev_1": {"alg1:k5": "fallback_key5"},
  451. "chris_dev_2": {"alg2:k6": "fallback_key6"},
  452. },
  453. }
  454. for user_id, devices in fallback_keys.items():
  455. for device_id, key_dict in devices.items():
  456. self.get_success(
  457. self.handler.upload_keys_for_user(
  458. user_id,
  459. device_id,
  460. {"fallback_keys": key_dict},
  461. )
  462. )
  463. # Each device should have an unused fallback key.
  464. for user_id, devices in fallback_keys.items():
  465. for device_id in devices:
  466. fallback_res = self.get_success(
  467. self.store.get_e2e_unused_fallback_key_types(user_id, device_id)
  468. )
  469. expected_algorithm_name = f"alg{device_id[-1]}"
  470. self.assertEqual(fallback_res, [expected_algorithm_name])
  471. # Claim the fallback key for one device per user.
  472. claim_res = self.get_success(
  473. self.handler.claim_one_time_keys(
  474. {
  475. alice: {"alice_dev_1": {"alg1": 1}},
  476. brian: {"brian_dev_2": {"alg2": 1}},
  477. chris: {"chris_dev_2": {"alg2": 1}},
  478. },
  479. self.requester,
  480. timeout=None,
  481. always_include_fallback_keys=False,
  482. )
  483. )
  484. expected_claims = {
  485. alice: {"alice_dev_1": {"alg1:k1": "fallback_key1"}},
  486. brian: {"brian_dev_2": {"alg2:k4": "fallback_key4"}},
  487. chris: {"chris_dev_2": {"alg2:k6": "fallback_key6"}},
  488. }
  489. self.assertEqual(
  490. claim_res,
  491. {"failures": {}, "one_time_keys": expected_claims},
  492. )
  493. for user_id, devices in fallback_keys.items():
  494. for device_id in devices:
  495. fallback_res = self.get_success(
  496. self.store.get_e2e_unused_fallback_key_types(user_id, device_id)
  497. )
  498. # Claimed fallback keys should no longer show up as unused.
  499. # Unclaimed fallback keys should still be unused.
  500. if device_id in expected_claims[user_id]:
  501. self.assertEqual(fallback_res, [])
  502. else:
  503. expected_algorithm_name = f"alg{device_id[-1]}"
  504. self.assertEqual(fallback_res, [expected_algorithm_name])
  505. def test_fallback_key_always_returned(self) -> None:
  506. local_user = "@boris:" + self.hs.hostname
  507. device_id = "xyz"
  508. fallback_key = {"alg1:k1": "fallback_key1"}
  509. otk = {"alg1:k2": "key2"}
  510. # we shouldn't have any unused fallback keys yet
  511. res = self.get_success(
  512. self.store.get_e2e_unused_fallback_key_types(local_user, device_id)
  513. )
  514. self.assertEqual(res, [])
  515. # Upload a OTK & fallback key.
  516. self.get_success(
  517. self.handler.upload_keys_for_user(
  518. local_user,
  519. device_id,
  520. {"one_time_keys": otk, "fallback_keys": fallback_key},
  521. )
  522. )
  523. # we should now have an unused alg1 key
  524. fallback_res = self.get_success(
  525. self.store.get_e2e_unused_fallback_key_types(local_user, device_id)
  526. )
  527. self.assertEqual(fallback_res, ["alg1"])
  528. # Claiming an OTK and requesting to always return the fallback key should
  529. # return both.
  530. claim_res = self.get_success(
  531. self.handler.claim_one_time_keys(
  532. {local_user: {device_id: {"alg1": 1}}},
  533. self.requester,
  534. timeout=None,
  535. always_include_fallback_keys=True,
  536. )
  537. )
  538. self.assertEqual(
  539. claim_res,
  540. {
  541. "failures": {},
  542. "one_time_keys": {local_user: {device_id: {**fallback_key, **otk}}},
  543. },
  544. )
  545. # This should not mark the key as used.
  546. fallback_res = self.get_success(
  547. self.store.get_e2e_unused_fallback_key_types(local_user, device_id)
  548. )
  549. self.assertEqual(fallback_res, ["alg1"])
  550. # Claiming an OTK again should return only the fallback key.
  551. claim_res = self.get_success(
  552. self.handler.claim_one_time_keys(
  553. {local_user: {device_id: {"alg1": 1}}},
  554. self.requester,
  555. timeout=None,
  556. always_include_fallback_keys=True,
  557. )
  558. )
  559. self.assertEqual(
  560. claim_res,
  561. {"failures": {}, "one_time_keys": {local_user: {device_id: fallback_key}}},
  562. )
  563. # And mark it as used.
  564. fallback_res = self.get_success(
  565. self.store.get_e2e_unused_fallback_key_types(local_user, device_id)
  566. )
  567. self.assertEqual(fallback_res, [])
  568. def test_replace_master_key(self) -> None:
  569. """uploading a new signing key should make the old signing key unavailable"""
  570. local_user = "@boris:" + self.hs.hostname
  571. keys1 = {
  572. "master_key": {
  573. # private key: 2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0
  574. "user_id": local_user,
  575. "usage": ["master"],
  576. "keys": {
  577. "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
  578. },
  579. }
  580. }
  581. self.get_success(self.handler.upload_signing_keys_for_user(local_user, keys1))
  582. keys2 = {
  583. "master_key": {
  584. # private key: 4TL4AjRYwDVwD3pqQzcor+ez/euOB1/q78aTJ+czDNs
  585. "user_id": local_user,
  586. "usage": ["master"],
  587. "keys": {
  588. "ed25519:Hq6gL+utB4ET+UvD5ci0kgAwsX6qP/zvf8v6OInU5iw": "Hq6gL+utB4ET+UvD5ci0kgAwsX6qP/zvf8v6OInU5iw"
  589. },
  590. }
  591. }
  592. self.get_success(self.handler.upload_signing_keys_for_user(local_user, keys2))
  593. devices = self.get_success(
  594. self.handler.query_devices(
  595. {"device_keys": {local_user: []}}, 0, local_user, "device123"
  596. )
  597. )
  598. self.assertDictEqual(devices["master_keys"], {local_user: keys2["master_key"]})
  599. def test_reupload_signatures(self) -> None:
  600. """re-uploading a signature should not fail"""
  601. local_user = "@boris:" + self.hs.hostname
  602. keys1 = {
  603. "master_key": {
  604. # private key: HvQBbU+hc2Zr+JP1sE0XwBe1pfZZEYtJNPJLZJtS+F8
  605. "user_id": local_user,
  606. "usage": ["master"],
  607. "keys": {
  608. "ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ": "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ"
  609. },
  610. },
  611. "self_signing_key": {
  612. # private key: 2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0
  613. "user_id": local_user,
  614. "usage": ["self_signing"],
  615. "keys": {
  616. "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
  617. },
  618. },
  619. }
  620. master_signing_key = key.decode_signing_key_base64(
  621. "ed25519",
  622. "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
  623. "HvQBbU+hc2Zr+JP1sE0XwBe1pfZZEYtJNPJLZJtS+F8",
  624. )
  625. sign.sign_json(keys1["self_signing_key"], local_user, master_signing_key)
  626. signing_key = key.decode_signing_key_base64(
  627. "ed25519",
  628. "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
  629. "2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0",
  630. )
  631. self.get_success(self.handler.upload_signing_keys_for_user(local_user, keys1))
  632. # upload two device keys, which will be signed later by the self-signing key
  633. device_key_1: JsonDict = {
  634. "user_id": local_user,
  635. "device_id": "abc",
  636. "algorithms": [
  637. "m.olm.curve25519-aes-sha2",
  638. RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
  639. ],
  640. "keys": {
  641. "ed25519:abc": "base64+ed25519+key",
  642. "curve25519:abc": "base64+curve25519+key",
  643. },
  644. "signatures": {local_user: {"ed25519:abc": "base64+signature"}},
  645. }
  646. device_key_2: JsonDict = {
  647. "user_id": local_user,
  648. "device_id": "def",
  649. "algorithms": [
  650. "m.olm.curve25519-aes-sha2",
  651. RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
  652. ],
  653. "keys": {
  654. "ed25519:def": "base64+ed25519+key",
  655. "curve25519:def": "base64+curve25519+key",
  656. },
  657. "signatures": {local_user: {"ed25519:def": "base64+signature"}},
  658. }
  659. self.get_success(
  660. self.handler.upload_keys_for_user(
  661. local_user, "abc", {"device_keys": device_key_1}
  662. )
  663. )
  664. self.get_success(
  665. self.handler.upload_keys_for_user(
  666. local_user, "def", {"device_keys": device_key_2}
  667. )
  668. )
  669. # sign the first device key and upload it
  670. del device_key_1["signatures"]
  671. sign.sign_json(device_key_1, local_user, signing_key)
  672. self.get_success(
  673. self.handler.upload_signatures_for_device_keys(
  674. local_user, {local_user: {"abc": device_key_1}}
  675. )
  676. )
  677. # sign the second device key and upload both device keys. The server
  678. # should ignore the first device key since it already has a valid
  679. # signature for it
  680. del device_key_2["signatures"]
  681. sign.sign_json(device_key_2, local_user, signing_key)
  682. self.get_success(
  683. self.handler.upload_signatures_for_device_keys(
  684. local_user, {local_user: {"abc": device_key_1, "def": device_key_2}}
  685. )
  686. )
  687. device_key_1["signatures"][local_user]["ed25519:abc"] = "base64+signature"
  688. device_key_2["signatures"][local_user]["ed25519:def"] = "base64+signature"
  689. devices = self.get_success(
  690. self.handler.query_devices(
  691. {"device_keys": {local_user: []}}, 0, local_user, "device123"
  692. )
  693. )
  694. del devices["device_keys"][local_user]["abc"]["unsigned"]
  695. del devices["device_keys"][local_user]["def"]["unsigned"]
  696. self.assertDictEqual(devices["device_keys"][local_user]["abc"], device_key_1)
  697. self.assertDictEqual(devices["device_keys"][local_user]["def"], device_key_2)
  698. def test_self_signing_key_doesnt_show_up_as_device(self) -> None:
  699. """signing keys should be hidden when fetching a user's devices"""
  700. local_user = "@boris:" + self.hs.hostname
  701. keys1 = {
  702. "master_key": {
  703. # private key: 2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0
  704. "user_id": local_user,
  705. "usage": ["master"],
  706. "keys": {
  707. "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
  708. },
  709. }
  710. }
  711. self.get_success(self.handler.upload_signing_keys_for_user(local_user, keys1))
  712. device_handler = self.hs.get_device_handler()
  713. assert isinstance(device_handler, DeviceHandler)
  714. e = self.get_failure(
  715. device_handler.check_device_registered(
  716. user_id=local_user,
  717. device_id="nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
  718. initial_device_display_name="new display name",
  719. ),
  720. SynapseError,
  721. )
  722. res = e.value.code
  723. self.assertEqual(res, 400)
  724. query_res = self.get_success(
  725. self.handler.query_local_devices({local_user: None})
  726. )
  727. self.assertDictEqual(query_res, {local_user: {}})
  728. def test_upload_signatures(self) -> None:
  729. """should check signatures that are uploaded"""
  730. # set up a user with cross-signing keys and a device. This user will
  731. # try uploading signatures
  732. local_user = "@boris:" + self.hs.hostname
  733. device_id = "xyz"
  734. # private key: OMkooTr76ega06xNvXIGPbgvvxAOzmQncN8VObS7aBA
  735. device_pubkey = "NnHhnqiMFQkq969szYkooLaBAXW244ZOxgukCvm2ZeY"
  736. device_key: JsonDict = {
  737. "user_id": local_user,
  738. "device_id": device_id,
  739. "algorithms": [
  740. "m.olm.curve25519-aes-sha2",
  741. RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
  742. ],
  743. "keys": {"curve25519:xyz": "curve25519+key", "ed25519:xyz": device_pubkey},
  744. "signatures": {local_user: {"ed25519:xyz": "something"}},
  745. }
  746. device_signing_key = key.decode_signing_key_base64(
  747. "ed25519", "xyz", "OMkooTr76ega06xNvXIGPbgvvxAOzmQncN8VObS7aBA"
  748. )
  749. self.get_success(
  750. self.handler.upload_keys_for_user(
  751. local_user, device_id, {"device_keys": device_key}
  752. )
  753. )
  754. # private key: 2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0
  755. master_pubkey = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
  756. master_key: JsonDict = {
  757. "user_id": local_user,
  758. "usage": ["master"],
  759. "keys": {"ed25519:" + master_pubkey: master_pubkey},
  760. }
  761. master_signing_key = key.decode_signing_key_base64(
  762. "ed25519", master_pubkey, "2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0"
  763. )
  764. usersigning_pubkey = "Hq6gL+utB4ET+UvD5ci0kgAwsX6qP/zvf8v6OInU5iw"
  765. usersigning_key = {
  766. # private key: 4TL4AjRYwDVwD3pqQzcor+ez/euOB1/q78aTJ+czDNs
  767. "user_id": local_user,
  768. "usage": ["user_signing"],
  769. "keys": {"ed25519:" + usersigning_pubkey: usersigning_pubkey},
  770. }
  771. usersigning_signing_key = key.decode_signing_key_base64(
  772. "ed25519", usersigning_pubkey, "4TL4AjRYwDVwD3pqQzcor+ez/euOB1/q78aTJ+czDNs"
  773. )
  774. sign.sign_json(usersigning_key, local_user, master_signing_key)
  775. # private key: HvQBbU+hc2Zr+JP1sE0XwBe1pfZZEYtJNPJLZJtS+F8
  776. selfsigning_pubkey = "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ"
  777. selfsigning_key = {
  778. "user_id": local_user,
  779. "usage": ["self_signing"],
  780. "keys": {"ed25519:" + selfsigning_pubkey: selfsigning_pubkey},
  781. }
  782. selfsigning_signing_key = key.decode_signing_key_base64(
  783. "ed25519", selfsigning_pubkey, "HvQBbU+hc2Zr+JP1sE0XwBe1pfZZEYtJNPJLZJtS+F8"
  784. )
  785. sign.sign_json(selfsigning_key, local_user, master_signing_key)
  786. cross_signing_keys = {
  787. "master_key": master_key,
  788. "user_signing_key": usersigning_key,
  789. "self_signing_key": selfsigning_key,
  790. }
  791. self.get_success(
  792. self.handler.upload_signing_keys_for_user(local_user, cross_signing_keys)
  793. )
  794. # set up another user with a master key. This user will be signed by
  795. # the first user
  796. other_user = "@otherboris:" + self.hs.hostname
  797. other_master_pubkey = "fHZ3NPiKxoLQm5OoZbKa99SYxprOjNs4TwJUKP+twCM"
  798. other_master_key: JsonDict = {
  799. # private key: oyw2ZUx0O4GifbfFYM0nQvj9CL0b8B7cyN4FprtK8OI
  800. "user_id": other_user,
  801. "usage": ["master"],
  802. "keys": {"ed25519:" + other_master_pubkey: other_master_pubkey},
  803. }
  804. self.get_success(
  805. self.handler.upload_signing_keys_for_user(
  806. other_user, {"master_key": other_master_key}
  807. )
  808. )
  809. # test various signature failures (see below)
  810. ret = self.get_success(
  811. self.handler.upload_signatures_for_device_keys(
  812. local_user,
  813. {
  814. local_user: {
  815. # fails because the signature is invalid
  816. # should fail with INVALID_SIGNATURE
  817. device_id: {
  818. "user_id": local_user,
  819. "device_id": device_id,
  820. "algorithms": [
  821. "m.olm.curve25519-aes-sha2",
  822. RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
  823. ],
  824. "keys": {
  825. "curve25519:xyz": "curve25519+key",
  826. # private key: OMkooTr76ega06xNvXIGPbgvvxAOzmQncN8VObS7aBA
  827. "ed25519:xyz": device_pubkey,
  828. },
  829. "signatures": {
  830. local_user: {
  831. "ed25519:" + selfsigning_pubkey: "something"
  832. }
  833. },
  834. },
  835. # fails because device is unknown
  836. # should fail with NOT_FOUND
  837. "unknown": {
  838. "user_id": local_user,
  839. "device_id": "unknown",
  840. "signatures": {
  841. local_user: {
  842. "ed25519:" + selfsigning_pubkey: "something"
  843. }
  844. },
  845. },
  846. # fails because the signature is invalid
  847. # should fail with INVALID_SIGNATURE
  848. master_pubkey: {
  849. "user_id": local_user,
  850. "usage": ["master"],
  851. "keys": {"ed25519:" + master_pubkey: master_pubkey},
  852. "signatures": {
  853. local_user: {"ed25519:" + device_pubkey: "something"}
  854. },
  855. },
  856. },
  857. other_user: {
  858. # fails because the device is not the user's master-signing key
  859. # should fail with NOT_FOUND
  860. "unknown": {
  861. "user_id": other_user,
  862. "device_id": "unknown",
  863. "signatures": {
  864. local_user: {
  865. "ed25519:" + usersigning_pubkey: "something"
  866. }
  867. },
  868. },
  869. other_master_pubkey: {
  870. # fails because the key doesn't match what the server has
  871. # should fail with UNKNOWN
  872. "user_id": other_user,
  873. "usage": ["master"],
  874. "keys": {
  875. "ed25519:" + other_master_pubkey: other_master_pubkey
  876. },
  877. "something": "random",
  878. "signatures": {
  879. local_user: {
  880. "ed25519:" + usersigning_pubkey: "something"
  881. }
  882. },
  883. },
  884. },
  885. },
  886. )
  887. )
  888. user_failures = ret["failures"][local_user]
  889. self.assertEqual(user_failures[device_id]["errcode"], Codes.INVALID_SIGNATURE)
  890. self.assertEqual(
  891. user_failures[master_pubkey]["errcode"], Codes.INVALID_SIGNATURE
  892. )
  893. self.assertEqual(user_failures["unknown"]["errcode"], Codes.NOT_FOUND)
  894. other_user_failures = ret["failures"][other_user]
  895. self.assertEqual(other_user_failures["unknown"]["errcode"], Codes.NOT_FOUND)
  896. self.assertEqual(
  897. other_user_failures[other_master_pubkey]["errcode"], Codes.UNKNOWN
  898. )
  899. # test successful signatures
  900. del device_key["signatures"]
  901. sign.sign_json(device_key, local_user, selfsigning_signing_key)
  902. sign.sign_json(master_key, local_user, device_signing_key)
  903. sign.sign_json(other_master_key, local_user, usersigning_signing_key)
  904. ret = self.get_success(
  905. self.handler.upload_signatures_for_device_keys(
  906. local_user,
  907. {
  908. local_user: {device_id: device_key, master_pubkey: master_key},
  909. other_user: {other_master_pubkey: other_master_key},
  910. },
  911. )
  912. )
  913. self.assertEqual(ret["failures"], {})
  914. # fetch the signed keys/devices and make sure that the signatures are there
  915. ret = self.get_success(
  916. self.handler.query_devices(
  917. {"device_keys": {local_user: [], other_user: []}},
  918. 0,
  919. local_user,
  920. "device123",
  921. )
  922. )
  923. self.assertEqual(
  924. ret["device_keys"][local_user]["xyz"]["signatures"][local_user][
  925. "ed25519:" + selfsigning_pubkey
  926. ],
  927. device_key["signatures"][local_user]["ed25519:" + selfsigning_pubkey],
  928. )
  929. self.assertEqual(
  930. ret["master_keys"][local_user]["signatures"][local_user][
  931. "ed25519:" + device_id
  932. ],
  933. master_key["signatures"][local_user]["ed25519:" + device_id],
  934. )
  935. self.assertEqual(
  936. ret["master_keys"][other_user]["signatures"][local_user][
  937. "ed25519:" + usersigning_pubkey
  938. ],
  939. other_master_key["signatures"][local_user]["ed25519:" + usersigning_pubkey],
  940. )
  941. def test_query_devices_remote_no_sync(self) -> None:
  942. """Tests that querying keys for a remote user that we don't share a room
  943. with returns the cross signing keys correctly.
  944. """
  945. remote_user_id = "@test:other"
  946. local_user_id = "@test:test"
  947. remote_master_key = "85T7JXPFBAySB/jwby4S3lBPTqY3+Zg53nYuGmu1ggY"
  948. remote_self_signing_key = "QeIiFEjluPBtI7WQdG365QKZcFs9kqmHir6RBD0//nQ"
  949. self.hs.get_federation_client().query_client_keys = mock.AsyncMock( # type: ignore[method-assign]
  950. return_value={
  951. "device_keys": {remote_user_id: {}},
  952. "master_keys": {
  953. remote_user_id: {
  954. "user_id": remote_user_id,
  955. "usage": ["master"],
  956. "keys": {"ed25519:" + remote_master_key: remote_master_key},
  957. },
  958. },
  959. "self_signing_keys": {
  960. remote_user_id: {
  961. "user_id": remote_user_id,
  962. "usage": ["self_signing"],
  963. "keys": {
  964. "ed25519:"
  965. + remote_self_signing_key: remote_self_signing_key
  966. },
  967. }
  968. },
  969. }
  970. )
  971. e2e_handler = self.hs.get_e2e_keys_handler()
  972. query_result = self.get_success(
  973. e2e_handler.query_devices(
  974. {
  975. "device_keys": {remote_user_id: []},
  976. },
  977. timeout=10,
  978. from_user_id=local_user_id,
  979. from_device_id="some_device_id",
  980. )
  981. )
  982. self.assertEqual(query_result["failures"], {})
  983. self.assertEqual(
  984. query_result["master_keys"],
  985. {
  986. remote_user_id: {
  987. "user_id": remote_user_id,
  988. "usage": ["master"],
  989. "keys": {"ed25519:" + remote_master_key: remote_master_key},
  990. },
  991. },
  992. )
  993. self.assertEqual(
  994. query_result["self_signing_keys"],
  995. {
  996. remote_user_id: {
  997. "user_id": remote_user_id,
  998. "usage": ["self_signing"],
  999. "keys": {
  1000. "ed25519:" + remote_self_signing_key: remote_self_signing_key
  1001. },
  1002. }
  1003. },
  1004. )
  1005. def test_query_devices_remote_sync(self) -> None:
  1006. """Tests that querying keys for a remote user that we share a room with,
  1007. but haven't yet fetched the keys for, returns the cross signing keys
  1008. correctly.
  1009. """
  1010. remote_user_id = "@test:other"
  1011. local_user_id = "@test:test"
  1012. # Pretend we're sharing a room with the user we're querying. If not,
  1013. # `_query_devices_for_destination` will return early.
  1014. self.store.get_rooms_for_user = mock.AsyncMock(return_value={"some_room_id"})
  1015. remote_master_key = "85T7JXPFBAySB/jwby4S3lBPTqY3+Zg53nYuGmu1ggY"
  1016. remote_self_signing_key = "QeIiFEjluPBtI7WQdG365QKZcFs9kqmHir6RBD0//nQ"
  1017. self.hs.get_federation_client().query_user_devices = mock.AsyncMock( # type: ignore[method-assign]
  1018. return_value={
  1019. "user_id": remote_user_id,
  1020. "stream_id": 1,
  1021. "devices": [],
  1022. "master_key": {
  1023. "user_id": remote_user_id,
  1024. "usage": ["master"],
  1025. "keys": {"ed25519:" + remote_master_key: remote_master_key},
  1026. },
  1027. "self_signing_key": {
  1028. "user_id": remote_user_id,
  1029. "usage": ["self_signing"],
  1030. "keys": {
  1031. "ed25519:" + remote_self_signing_key: remote_self_signing_key
  1032. },
  1033. },
  1034. }
  1035. )
  1036. e2e_handler = self.hs.get_e2e_keys_handler()
  1037. query_result = self.get_success(
  1038. e2e_handler.query_devices(
  1039. {
  1040. "device_keys": {remote_user_id: []},
  1041. },
  1042. timeout=10,
  1043. from_user_id=local_user_id,
  1044. from_device_id="some_device_id",
  1045. )
  1046. )
  1047. self.assertEqual(query_result["failures"], {})
  1048. self.assertEqual(
  1049. query_result["master_keys"],
  1050. {
  1051. remote_user_id: {
  1052. "user_id": remote_user_id,
  1053. "usage": ["master"],
  1054. "keys": {"ed25519:" + remote_master_key: remote_master_key},
  1055. }
  1056. },
  1057. )
  1058. self.assertEqual(
  1059. query_result["self_signing_keys"],
  1060. {
  1061. remote_user_id: {
  1062. "user_id": remote_user_id,
  1063. "usage": ["self_signing"],
  1064. "keys": {
  1065. "ed25519:" + remote_self_signing_key: remote_self_signing_key
  1066. },
  1067. }
  1068. },
  1069. )
  1070. @parameterized.expand(
  1071. [
  1072. # The remote homeserver's response indicates that this user has 0/1/2 devices.
  1073. ([],),
  1074. (["device_1"],),
  1075. (["device_1", "device_2"],),
  1076. ]
  1077. )
  1078. def test_query_all_devices_caches_result(self, device_ids: Iterable[str]) -> None:
  1079. """Test that requests for all of a remote user's devices are cached.
  1080. We do this by asserting that only one call over federation was made, and that
  1081. the two queries to the local homeserver produce the same response.
  1082. """
  1083. local_user_id = "@test:test"
  1084. remote_user_id = "@test:other"
  1085. request_body: JsonDict = {"device_keys": {remote_user_id: []}}
  1086. response_devices = [
  1087. {
  1088. "device_id": device_id,
  1089. "keys": {
  1090. "algorithms": ["dummy"],
  1091. "device_id": device_id,
  1092. "keys": {f"dummy:{device_id}": "dummy"},
  1093. "signatures": {device_id: {f"dummy:{device_id}": "dummy"}},
  1094. "unsigned": {},
  1095. "user_id": "@test:other",
  1096. },
  1097. }
  1098. for device_id in device_ids
  1099. ]
  1100. response_body = {
  1101. "devices": response_devices,
  1102. "user_id": remote_user_id,
  1103. "stream_id": 12345, # an integer, according to the spec
  1104. }
  1105. e2e_handler = self.hs.get_e2e_keys_handler()
  1106. # Pretend we're sharing a room with the user we're querying. If not,
  1107. # `_query_devices_for_destination` will return early.
  1108. mock_get_rooms = mock.patch.object(
  1109. self.store,
  1110. "get_rooms_for_user",
  1111. new_callable=mock.AsyncMock,
  1112. return_value=["some_room_id"],
  1113. )
  1114. mock_get_users = mock.patch.object(
  1115. self.store,
  1116. "get_users_server_still_shares_room_with",
  1117. new_callable=mock.AsyncMock,
  1118. return_value={remote_user_id},
  1119. )
  1120. mock_request = mock.patch.object(
  1121. self.hs.get_federation_client(),
  1122. "query_user_devices",
  1123. new_callable=mock.AsyncMock,
  1124. return_value=response_body,
  1125. )
  1126. with mock_get_rooms, mock_get_users, mock_request as mocked_federation_request:
  1127. # Make the first query and sanity check it succeeds.
  1128. response_1 = self.get_success(
  1129. e2e_handler.query_devices(
  1130. request_body,
  1131. timeout=10,
  1132. from_user_id=local_user_id,
  1133. from_device_id="some_device_id",
  1134. )
  1135. )
  1136. self.assertEqual(response_1["failures"], {})
  1137. # We should have made a federation request to do so.
  1138. mocked_federation_request.assert_called_once()
  1139. # Reset the mock so we can prove we don't make a second federation request.
  1140. mocked_federation_request.reset_mock()
  1141. # Repeat the query.
  1142. response_2 = self.get_success(
  1143. e2e_handler.query_devices(
  1144. request_body,
  1145. timeout=10,
  1146. from_user_id=local_user_id,
  1147. from_device_id="some_device_id",
  1148. )
  1149. )
  1150. self.assertEqual(response_2["failures"], {})
  1151. # We should not have made a second federation request.
  1152. mocked_federation_request.assert_not_called()
  1153. # The two requests to the local homeserver should be identical.
  1154. self.assertEqual(response_1, response_2)
  1155. @override_config({"experimental_features": {"msc3983_appservice_otk_claims": True}})
  1156. def test_query_appservice(self) -> None:
  1157. local_user = "@boris:" + self.hs.hostname
  1158. device_id_1 = "xyz"
  1159. fallback_key = {"alg1:k1": "fallback_key1"}
  1160. device_id_2 = "abc"
  1161. otk = {"alg1:k2": "key2"}
  1162. # Inject an appservice interested in this user.
  1163. appservice = ApplicationService(
  1164. token="i_am_an_app_service",
  1165. id="1234",
  1166. namespaces={"users": [{"regex": r"@boris:.+", "exclusive": True}]},
  1167. # Note: this user does not have to match the regex above
  1168. sender="@as_main:test",
  1169. )
  1170. self.hs.get_datastores().main.services_cache = [appservice]
  1171. self.hs.get_datastores().main.exclusive_user_regex = _make_exclusive_regex(
  1172. [appservice]
  1173. )
  1174. # Setup a response, but only for device 2.
  1175. self.appservice_api.claim_client_keys.return_value = (
  1176. {local_user: {device_id_2: otk}},
  1177. [(local_user, device_id_1, "alg1", 1)],
  1178. )
  1179. # we shouldn't have any unused fallback keys yet
  1180. res = self.get_success(
  1181. self.store.get_e2e_unused_fallback_key_types(local_user, device_id_1)
  1182. )
  1183. self.assertEqual(res, [])
  1184. self.get_success(
  1185. self.handler.upload_keys_for_user(
  1186. local_user,
  1187. device_id_1,
  1188. {"fallback_keys": fallback_key},
  1189. )
  1190. )
  1191. # we should now have an unused alg1 key
  1192. fallback_res = self.get_success(
  1193. self.store.get_e2e_unused_fallback_key_types(local_user, device_id_1)
  1194. )
  1195. self.assertEqual(fallback_res, ["alg1"])
  1196. # claiming an OTK when no OTKs are available should ask the appservice, then
  1197. # query the fallback keys.
  1198. claim_res = self.get_success(
  1199. self.handler.claim_one_time_keys(
  1200. {local_user: {device_id_1: {"alg1": 1}, device_id_2: {"alg1": 1}}},
  1201. self.requester,
  1202. timeout=None,
  1203. always_include_fallback_keys=False,
  1204. )
  1205. )
  1206. self.assertEqual(
  1207. claim_res,
  1208. {
  1209. "failures": {},
  1210. "one_time_keys": {
  1211. local_user: {device_id_1: fallback_key, device_id_2: otk}
  1212. },
  1213. },
  1214. )
  1215. @override_config({"experimental_features": {"msc3983_appservice_otk_claims": True}})
  1216. def test_query_appservice_with_fallback(self) -> None:
  1217. local_user = "@boris:" + self.hs.hostname
  1218. device_id_1 = "xyz"
  1219. fallback_key = {"alg1:k1": {"desc": "fallback_key1", "fallback": True}}
  1220. otk = {"alg1:k2": {"desc": "key2"}}
  1221. as_fallback_key = {"alg1:k3": {"desc": "fallback_key3", "fallback": True}}
  1222. as_otk = {"alg1:k4": {"desc": "key4"}}
  1223. # Inject an appservice interested in this user.
  1224. appservice = ApplicationService(
  1225. token="i_am_an_app_service",
  1226. id="1234",
  1227. namespaces={"users": [{"regex": r"@boris:.+", "exclusive": True}]},
  1228. # Note: this user does not have to match the regex above
  1229. sender="@as_main:test",
  1230. )
  1231. self.hs.get_datastores().main.services_cache = [appservice]
  1232. self.hs.get_datastores().main.exclusive_user_regex = _make_exclusive_regex(
  1233. [appservice]
  1234. )
  1235. # Setup a response.
  1236. response: Dict[str, Dict[str, Dict[str, JsonDict]]] = {
  1237. local_user: {device_id_1: {**as_otk, **as_fallback_key}}
  1238. }
  1239. self.appservice_api.claim_client_keys.return_value = (response, [])
  1240. # Claim OTKs, which will ask the appservice and do nothing else.
  1241. claim_res = self.get_success(
  1242. self.handler.claim_one_time_keys(
  1243. {local_user: {device_id_1: {"alg1": 1}}},
  1244. self.requester,
  1245. timeout=None,
  1246. always_include_fallback_keys=True,
  1247. )
  1248. )
  1249. self.assertEqual(
  1250. claim_res,
  1251. {
  1252. "failures": {},
  1253. "one_time_keys": {
  1254. local_user: {device_id_1: {**as_otk, **as_fallback_key}}
  1255. },
  1256. },
  1257. )
  1258. # Now upload a fallback key.
  1259. res = self.get_success(
  1260. self.store.get_e2e_unused_fallback_key_types(local_user, device_id_1)
  1261. )
  1262. self.assertEqual(res, [])
  1263. self.get_success(
  1264. self.handler.upload_keys_for_user(
  1265. local_user,
  1266. device_id_1,
  1267. {"fallback_keys": fallback_key},
  1268. )
  1269. )
  1270. # we should now have an unused alg1 key
  1271. fallback_res = self.get_success(
  1272. self.store.get_e2e_unused_fallback_key_types(local_user, device_id_1)
  1273. )
  1274. self.assertEqual(fallback_res, ["alg1"])
  1275. # The appservice will return only the OTK.
  1276. self.appservice_api.claim_client_keys.return_value = (
  1277. {local_user: {device_id_1: as_otk}},
  1278. [],
  1279. )
  1280. # Claim OTKs, which should return the OTK from the appservice and the
  1281. # uploaded fallback key.
  1282. claim_res = self.get_success(
  1283. self.handler.claim_one_time_keys(
  1284. {local_user: {device_id_1: {"alg1": 1}}},
  1285. self.requester,
  1286. timeout=None,
  1287. always_include_fallback_keys=True,
  1288. )
  1289. )
  1290. self.assertEqual(
  1291. claim_res,
  1292. {
  1293. "failures": {},
  1294. "one_time_keys": {
  1295. local_user: {device_id_1: {**as_otk, **fallback_key}}
  1296. },
  1297. },
  1298. )
  1299. # But the fallback key should not be marked as used.
  1300. fallback_res = self.get_success(
  1301. self.store.get_e2e_unused_fallback_key_types(local_user, device_id_1)
  1302. )
  1303. self.assertEqual(fallback_res, ["alg1"])
  1304. # Now upload a OTK.
  1305. self.get_success(
  1306. self.handler.upload_keys_for_user(
  1307. local_user,
  1308. device_id_1,
  1309. {"one_time_keys": otk},
  1310. )
  1311. )
  1312. # Claim OTKs, which will return information only from the database.
  1313. claim_res = self.get_success(
  1314. self.handler.claim_one_time_keys(
  1315. {local_user: {device_id_1: {"alg1": 1}}},
  1316. self.requester,
  1317. timeout=None,
  1318. always_include_fallback_keys=True,
  1319. )
  1320. )
  1321. self.assertEqual(
  1322. claim_res,
  1323. {
  1324. "failures": {},
  1325. "one_time_keys": {local_user: {device_id_1: {**otk, **fallback_key}}},
  1326. },
  1327. )
  1328. # But the fallback key should not be marked as used.
  1329. fallback_res = self.get_success(
  1330. self.store.get_e2e_unused_fallback_key_types(local_user, device_id_1)
  1331. )
  1332. self.assertEqual(fallback_res, ["alg1"])
  1333. # Finally, return only the fallback key from the appservice.
  1334. self.appservice_api.claim_client_keys.return_value = (
  1335. {local_user: {device_id_1: as_fallback_key}},
  1336. [],
  1337. )
  1338. # Claim OTKs, which will return only the fallback key from the database.
  1339. claim_res = self.get_success(
  1340. self.handler.claim_one_time_keys(
  1341. {local_user: {device_id_1: {"alg1": 1}}},
  1342. self.requester,
  1343. timeout=None,
  1344. always_include_fallback_keys=True,
  1345. )
  1346. )
  1347. self.assertEqual(
  1348. claim_res,
  1349. {
  1350. "failures": {},
  1351. "one_time_keys": {local_user: {device_id_1: as_fallback_key}},
  1352. },
  1353. )
  1354. @override_config({"experimental_features": {"msc3984_appservice_key_query": True}})
  1355. def test_query_local_devices_appservice(self) -> None:
  1356. """Test that querying of appservices for keys overrides responses from the database."""
  1357. local_user = "@boris:" + self.hs.hostname
  1358. device_1 = "abc"
  1359. device_2 = "def"
  1360. device_3 = "ghi"
  1361. # There are 3 devices:
  1362. #
  1363. # 1. One which is uploaded to the homeserver.
  1364. # 2. One which is uploaded to the homeserver, but a newer copy is returned
  1365. # by the appservice.
  1366. # 3. One which is only returned by the appservice.
  1367. device_key_1: JsonDict = {
  1368. "user_id": local_user,
  1369. "device_id": device_1,
  1370. "algorithms": [
  1371. "m.olm.curve25519-aes-sha2",
  1372. RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
  1373. ],
  1374. "keys": {
  1375. "ed25519:abc": "base64+ed25519+key",
  1376. "curve25519:abc": "base64+curve25519+key",
  1377. },
  1378. "signatures": {local_user: {"ed25519:abc": "base64+signature"}},
  1379. }
  1380. device_key_2a: JsonDict = {
  1381. "user_id": local_user,
  1382. "device_id": device_2,
  1383. "algorithms": [
  1384. "m.olm.curve25519-aes-sha2",
  1385. RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
  1386. ],
  1387. "keys": {
  1388. "ed25519:def": "base64+ed25519+key",
  1389. "curve25519:def": "base64+curve25519+key",
  1390. },
  1391. "signatures": {local_user: {"ed25519:def": "base64+signature"}},
  1392. }
  1393. device_key_2b: JsonDict = {
  1394. "user_id": local_user,
  1395. "device_id": device_2,
  1396. "algorithms": [
  1397. "m.olm.curve25519-aes-sha2",
  1398. RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
  1399. ],
  1400. # The device ID is the same (above), but the keys are different.
  1401. "keys": {
  1402. "ed25519:xyz": "base64+ed25519+key",
  1403. "curve25519:xyz": "base64+curve25519+key",
  1404. },
  1405. "signatures": {local_user: {"ed25519:xyz": "base64+signature"}},
  1406. }
  1407. device_key_3: JsonDict = {
  1408. "user_id": local_user,
  1409. "device_id": device_3,
  1410. "algorithms": [
  1411. "m.olm.curve25519-aes-sha2",
  1412. RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
  1413. ],
  1414. "keys": {
  1415. "ed25519:jkl": "base64+ed25519+key",
  1416. "curve25519:jkl": "base64+curve25519+key",
  1417. },
  1418. "signatures": {local_user: {"ed25519:jkl": "base64+signature"}},
  1419. }
  1420. # Upload keys for devices 1 & 2a.
  1421. self.get_success(
  1422. self.handler.upload_keys_for_user(
  1423. local_user, device_1, {"device_keys": device_key_1}
  1424. )
  1425. )
  1426. self.get_success(
  1427. self.handler.upload_keys_for_user(
  1428. local_user, device_2, {"device_keys": device_key_2a}
  1429. )
  1430. )
  1431. # Inject an appservice interested in this user.
  1432. appservice = ApplicationService(
  1433. token="i_am_an_app_service",
  1434. id="1234",
  1435. namespaces={"users": [{"regex": r"@boris:.+", "exclusive": True}]},
  1436. # Note: this user does not have to match the regex above
  1437. sender="@as_main:test",
  1438. )
  1439. self.hs.get_datastores().main.services_cache = [appservice]
  1440. self.hs.get_datastores().main.exclusive_user_regex = _make_exclusive_regex(
  1441. [appservice]
  1442. )
  1443. # Setup a response.
  1444. self.appservice_api.query_keys.return_value = {
  1445. "device_keys": {
  1446. local_user: {device_2: device_key_2b, device_3: device_key_3}
  1447. }
  1448. }
  1449. # Request all devices.
  1450. res = self.get_success(self.handler.query_local_devices({local_user: None}))
  1451. self.assertIn(local_user, res)
  1452. for res_key in res[local_user].values():
  1453. res_key.pop("unsigned", None)
  1454. self.assertDictEqual(
  1455. res,
  1456. {
  1457. local_user: {
  1458. device_1: device_key_1,
  1459. device_2: device_key_2b,
  1460. device_3: device_key_3,
  1461. }
  1462. },
  1463. )
  1464. def test_check_cross_signing_setup(self) -> None:
  1465. # First check what happens with no master key.
  1466. alice = "@alice:test"
  1467. exists, replaceable_without_uia = self.get_success(
  1468. self.handler.check_cross_signing_setup(alice)
  1469. )
  1470. self.assertIs(exists, False)
  1471. self.assertIs(replaceable_without_uia, False)
  1472. # Upload a master key but don't specify a replacement timestamp.
  1473. dummy_key = {"keys": {"a": "b"}}
  1474. self.get_success(
  1475. self.store.set_e2e_cross_signing_key("@alice:test", "master", dummy_key)
  1476. )
  1477. # Should now find the key exists.
  1478. exists, replaceable_without_uia = self.get_success(
  1479. self.handler.check_cross_signing_setup(alice)
  1480. )
  1481. self.assertIs(exists, True)
  1482. self.assertIs(replaceable_without_uia, False)
  1483. # Set an expiry timestamp in the future.
  1484. self.get_success(
  1485. self.store.allow_master_cross_signing_key_replacement_without_uia(
  1486. alice,
  1487. 1000,
  1488. )
  1489. )
  1490. # Should now be allowed to replace the key without UIA.
  1491. exists, replaceable_without_uia = self.get_success(
  1492. self.handler.check_cross_signing_setup(alice)
  1493. )
  1494. self.assertIs(exists, True)
  1495. self.assertIs(replaceable_without_uia, True)
  1496. # Wait 2 seconds, so that the timestamp is in the past.
  1497. self.reactor.advance(2.0)
  1498. # Should no longer be allowed to replace the key without UIA.
  1499. exists, replaceable_without_uia = self.get_success(
  1500. self.handler.check_cross_signing_setup(alice)
  1501. )
  1502. self.assertIs(exists, True)
  1503. self.assertIs(replaceable_without_uia, False)