test_e2e_keys.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934
  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 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.server import HomeServer
  24. from synapse.types import JsonDict
  25. from synapse.util import Clock
  26. from tests import unittest
  27. from tests.test_utils import make_awaitable
  28. class E2eKeysHandlerTestCase(unittest.HomeserverTestCase):
  29. def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
  30. return self.setup_test_homeserver(federation_client=mock.Mock())
  31. def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
  32. self.handler = hs.get_e2e_keys_handler()
  33. self.store = self.hs.get_datastores().main
  34. def test_query_local_devices_no_devices(self) -> None:
  35. """If the user has no devices, we expect an empty list."""
  36. local_user = "@boris:" + self.hs.hostname
  37. res = self.get_success(self.handler.query_local_devices({local_user: None}))
  38. self.assertDictEqual(res, {local_user: {}})
  39. def test_reupload_one_time_keys(self) -> None:
  40. """we should be able to re-upload the same keys"""
  41. local_user = "@boris:" + self.hs.hostname
  42. device_id = "xyz"
  43. keys: JsonDict = {
  44. "alg1:k1": "key1",
  45. "alg2:k2": {"key": "key2", "signatures": {"k1": "sig1"}},
  46. "alg2:k3": {"key": "key3"},
  47. }
  48. # Note that "signed_curve25519" is always returned in key count responses. This is necessary until
  49. # https://github.com/matrix-org/matrix-doc/issues/3298 is fixed.
  50. res = self.get_success(
  51. self.handler.upload_keys_for_user(
  52. local_user, device_id, {"one_time_keys": keys}
  53. )
  54. )
  55. self.assertDictEqual(
  56. res, {"one_time_key_counts": {"alg1": 1, "alg2": 2, "signed_curve25519": 0}}
  57. )
  58. # we should be able to change the signature without a problem
  59. keys["alg2:k2"]["signatures"]["k1"] = "sig2"
  60. res = self.get_success(
  61. self.handler.upload_keys_for_user(
  62. local_user, device_id, {"one_time_keys": keys}
  63. )
  64. )
  65. self.assertDictEqual(
  66. res, {"one_time_key_counts": {"alg1": 1, "alg2": 2, "signed_curve25519": 0}}
  67. )
  68. def test_change_one_time_keys(self) -> None:
  69. """attempts to change one-time-keys should be rejected"""
  70. local_user = "@boris:" + self.hs.hostname
  71. device_id = "xyz"
  72. keys = {
  73. "alg1:k1": "key1",
  74. "alg2:k2": {"key": "key2", "signatures": {"k1": "sig1"}},
  75. "alg2:k3": {"key": "key3"},
  76. }
  77. res = self.get_success(
  78. self.handler.upload_keys_for_user(
  79. local_user, device_id, {"one_time_keys": keys}
  80. )
  81. )
  82. self.assertDictEqual(
  83. res, {"one_time_key_counts": {"alg1": 1, "alg2": 2, "signed_curve25519": 0}}
  84. )
  85. # Error when changing string key
  86. self.get_failure(
  87. self.handler.upload_keys_for_user(
  88. local_user, device_id, {"one_time_keys": {"alg1:k1": "key2"}}
  89. ),
  90. SynapseError,
  91. )
  92. # Error when replacing dict key with string
  93. self.get_failure(
  94. self.handler.upload_keys_for_user(
  95. local_user, device_id, {"one_time_keys": {"alg2:k3": "key2"}}
  96. ),
  97. SynapseError,
  98. )
  99. # Error when replacing string key with dict
  100. self.get_failure(
  101. self.handler.upload_keys_for_user(
  102. local_user,
  103. device_id,
  104. {"one_time_keys": {"alg1:k1": {"key": "key"}}},
  105. ),
  106. SynapseError,
  107. )
  108. # Error when replacing dict key
  109. self.get_failure(
  110. self.handler.upload_keys_for_user(
  111. local_user,
  112. device_id,
  113. {
  114. "one_time_keys": {
  115. "alg2:k2": {"key": "key3", "signatures": {"k1": "sig1"}}
  116. }
  117. },
  118. ),
  119. SynapseError,
  120. )
  121. def test_claim_one_time_key(self) -> None:
  122. local_user = "@boris:" + self.hs.hostname
  123. device_id = "xyz"
  124. keys = {"alg1:k1": "key1"}
  125. res = self.get_success(
  126. self.handler.upload_keys_for_user(
  127. local_user, device_id, {"one_time_keys": keys}
  128. )
  129. )
  130. self.assertDictEqual(
  131. res, {"one_time_key_counts": {"alg1": 1, "signed_curve25519": 0}}
  132. )
  133. res2 = self.get_success(
  134. self.handler.claim_one_time_keys(
  135. {"one_time_keys": {local_user: {device_id: "alg1"}}}, timeout=None
  136. )
  137. )
  138. self.assertEqual(
  139. res2,
  140. {
  141. "failures": {},
  142. "one_time_keys": {local_user: {device_id: {"alg1:k1": "key1"}}},
  143. },
  144. )
  145. def test_fallback_key(self) -> None:
  146. local_user = "@boris:" + self.hs.hostname
  147. device_id = "xyz"
  148. fallback_key = {"alg1:k1": "fallback_key1"}
  149. fallback_key2 = {"alg1:k2": "fallback_key2"}
  150. fallback_key3 = {"alg1:k2": "fallback_key3"}
  151. otk = {"alg1:k2": "key2"}
  152. # we shouldn't have any unused fallback keys yet
  153. res = self.get_success(
  154. self.store.get_e2e_unused_fallback_key_types(local_user, device_id)
  155. )
  156. self.assertEqual(res, [])
  157. self.get_success(
  158. self.handler.upload_keys_for_user(
  159. local_user,
  160. device_id,
  161. {"fallback_keys": fallback_key},
  162. )
  163. )
  164. # we should now have an unused alg1 key
  165. res = self.get_success(
  166. self.store.get_e2e_unused_fallback_key_types(local_user, device_id)
  167. )
  168. self.assertEqual(res, ["alg1"])
  169. # claiming an OTK when no OTKs are available should return the fallback
  170. # key
  171. res = self.get_success(
  172. self.handler.claim_one_time_keys(
  173. {"one_time_keys": {local_user: {device_id: "alg1"}}}, timeout=None
  174. )
  175. )
  176. self.assertEqual(
  177. res,
  178. {"failures": {}, "one_time_keys": {local_user: {device_id: fallback_key}}},
  179. )
  180. # we shouldn't have any unused fallback keys again
  181. res = self.get_success(
  182. self.store.get_e2e_unused_fallback_key_types(local_user, device_id)
  183. )
  184. self.assertEqual(res, [])
  185. # claiming an OTK again should return the same fallback key
  186. res = self.get_success(
  187. self.handler.claim_one_time_keys(
  188. {"one_time_keys": {local_user: {device_id: "alg1"}}}, timeout=None
  189. )
  190. )
  191. self.assertEqual(
  192. res,
  193. {"failures": {}, "one_time_keys": {local_user: {device_id: fallback_key}}},
  194. )
  195. # re-uploading the same fallback key should still result in no unused fallback
  196. # keys
  197. self.get_success(
  198. self.handler.upload_keys_for_user(
  199. local_user,
  200. device_id,
  201. {"fallback_keys": fallback_key},
  202. )
  203. )
  204. res = self.get_success(
  205. self.store.get_e2e_unused_fallback_key_types(local_user, device_id)
  206. )
  207. self.assertEqual(res, [])
  208. # uploading a new fallback key should result in an unused fallback key
  209. self.get_success(
  210. self.handler.upload_keys_for_user(
  211. local_user,
  212. device_id,
  213. {"fallback_keys": fallback_key2},
  214. )
  215. )
  216. res = self.get_success(
  217. self.store.get_e2e_unused_fallback_key_types(local_user, device_id)
  218. )
  219. self.assertEqual(res, ["alg1"])
  220. # if the user uploads a one-time key, the next claim should fetch the
  221. # one-time key, and then go back to the fallback
  222. self.get_success(
  223. self.handler.upload_keys_for_user(
  224. local_user, device_id, {"one_time_keys": otk}
  225. )
  226. )
  227. res = self.get_success(
  228. self.handler.claim_one_time_keys(
  229. {"one_time_keys": {local_user: {device_id: "alg1"}}}, timeout=None
  230. )
  231. )
  232. self.assertEqual(
  233. res,
  234. {"failures": {}, "one_time_keys": {local_user: {device_id: otk}}},
  235. )
  236. res = self.get_success(
  237. self.handler.claim_one_time_keys(
  238. {"one_time_keys": {local_user: {device_id: "alg1"}}}, timeout=None
  239. )
  240. )
  241. self.assertEqual(
  242. res,
  243. {"failures": {}, "one_time_keys": {local_user: {device_id: fallback_key2}}},
  244. )
  245. # using the unstable prefix should also set the fallback key
  246. self.get_success(
  247. self.handler.upload_keys_for_user(
  248. local_user,
  249. device_id,
  250. {"org.matrix.msc2732.fallback_keys": fallback_key3},
  251. )
  252. )
  253. res = self.get_success(
  254. self.handler.claim_one_time_keys(
  255. {"one_time_keys": {local_user: {device_id: "alg1"}}}, timeout=None
  256. )
  257. )
  258. self.assertEqual(
  259. res,
  260. {"failures": {}, "one_time_keys": {local_user: {device_id: fallback_key3}}},
  261. )
  262. def test_replace_master_key(self) -> None:
  263. """uploading a new signing key should make the old signing key unavailable"""
  264. local_user = "@boris:" + self.hs.hostname
  265. keys1 = {
  266. "master_key": {
  267. # private key: 2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0
  268. "user_id": local_user,
  269. "usage": ["master"],
  270. "keys": {
  271. "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
  272. },
  273. }
  274. }
  275. self.get_success(self.handler.upload_signing_keys_for_user(local_user, keys1))
  276. keys2 = {
  277. "master_key": {
  278. # private key: 4TL4AjRYwDVwD3pqQzcor+ez/euOB1/q78aTJ+czDNs
  279. "user_id": local_user,
  280. "usage": ["master"],
  281. "keys": {
  282. "ed25519:Hq6gL+utB4ET+UvD5ci0kgAwsX6qP/zvf8v6OInU5iw": "Hq6gL+utB4ET+UvD5ci0kgAwsX6qP/zvf8v6OInU5iw"
  283. },
  284. }
  285. }
  286. self.get_success(self.handler.upload_signing_keys_for_user(local_user, keys2))
  287. devices = self.get_success(
  288. self.handler.query_devices(
  289. {"device_keys": {local_user: []}}, 0, local_user, "device123"
  290. )
  291. )
  292. self.assertDictEqual(devices["master_keys"], {local_user: keys2["master_key"]})
  293. def test_reupload_signatures(self) -> None:
  294. """re-uploading a signature should not fail"""
  295. local_user = "@boris:" + self.hs.hostname
  296. keys1 = {
  297. "master_key": {
  298. # private key: HvQBbU+hc2Zr+JP1sE0XwBe1pfZZEYtJNPJLZJtS+F8
  299. "user_id": local_user,
  300. "usage": ["master"],
  301. "keys": {
  302. "ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ": "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ"
  303. },
  304. },
  305. "self_signing_key": {
  306. # private key: 2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0
  307. "user_id": local_user,
  308. "usage": ["self_signing"],
  309. "keys": {
  310. "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
  311. },
  312. },
  313. }
  314. master_signing_key = key.decode_signing_key_base64(
  315. "ed25519",
  316. "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
  317. "HvQBbU+hc2Zr+JP1sE0XwBe1pfZZEYtJNPJLZJtS+F8",
  318. )
  319. sign.sign_json(keys1["self_signing_key"], local_user, master_signing_key)
  320. signing_key = key.decode_signing_key_base64(
  321. "ed25519",
  322. "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
  323. "2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0",
  324. )
  325. self.get_success(self.handler.upload_signing_keys_for_user(local_user, keys1))
  326. # upload two device keys, which will be signed later by the self-signing key
  327. device_key_1 = {
  328. "user_id": local_user,
  329. "device_id": "abc",
  330. "algorithms": [
  331. "m.olm.curve25519-aes-sha2",
  332. RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
  333. ],
  334. "keys": {
  335. "ed25519:abc": "base64+ed25519+key",
  336. "curve25519:abc": "base64+curve25519+key",
  337. },
  338. "signatures": {local_user: {"ed25519:abc": "base64+signature"}},
  339. }
  340. device_key_2 = {
  341. "user_id": local_user,
  342. "device_id": "def",
  343. "algorithms": [
  344. "m.olm.curve25519-aes-sha2",
  345. RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
  346. ],
  347. "keys": {
  348. "ed25519:def": "base64+ed25519+key",
  349. "curve25519:def": "base64+curve25519+key",
  350. },
  351. "signatures": {local_user: {"ed25519:def": "base64+signature"}},
  352. }
  353. self.get_success(
  354. self.handler.upload_keys_for_user(
  355. local_user, "abc", {"device_keys": device_key_1}
  356. )
  357. )
  358. self.get_success(
  359. self.handler.upload_keys_for_user(
  360. local_user, "def", {"device_keys": device_key_2}
  361. )
  362. )
  363. # sign the first device key and upload it
  364. del device_key_1["signatures"]
  365. sign.sign_json(device_key_1, local_user, signing_key)
  366. self.get_success(
  367. self.handler.upload_signatures_for_device_keys(
  368. local_user, {local_user: {"abc": device_key_1}}
  369. )
  370. )
  371. # sign the second device key and upload both device keys. The server
  372. # should ignore the first device key since it already has a valid
  373. # signature for it
  374. del device_key_2["signatures"]
  375. sign.sign_json(device_key_2, local_user, signing_key)
  376. self.get_success(
  377. self.handler.upload_signatures_for_device_keys(
  378. local_user, {local_user: {"abc": device_key_1, "def": device_key_2}}
  379. )
  380. )
  381. device_key_1["signatures"][local_user]["ed25519:abc"] = "base64+signature"
  382. device_key_2["signatures"][local_user]["ed25519:def"] = "base64+signature"
  383. devices = self.get_success(
  384. self.handler.query_devices(
  385. {"device_keys": {local_user: []}}, 0, local_user, "device123"
  386. )
  387. )
  388. del devices["device_keys"][local_user]["abc"]["unsigned"]
  389. del devices["device_keys"][local_user]["def"]["unsigned"]
  390. self.assertDictEqual(devices["device_keys"][local_user]["abc"], device_key_1)
  391. self.assertDictEqual(devices["device_keys"][local_user]["def"], device_key_2)
  392. def test_self_signing_key_doesnt_show_up_as_device(self) -> None:
  393. """signing keys should be hidden when fetching a user's devices"""
  394. local_user = "@boris:" + self.hs.hostname
  395. keys1 = {
  396. "master_key": {
  397. # private key: 2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0
  398. "user_id": local_user,
  399. "usage": ["master"],
  400. "keys": {
  401. "ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk": "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
  402. },
  403. }
  404. }
  405. self.get_success(self.handler.upload_signing_keys_for_user(local_user, keys1))
  406. e = self.get_failure(
  407. self.hs.get_device_handler().check_device_registered(
  408. user_id=local_user,
  409. device_id="nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
  410. initial_device_display_name="new display name",
  411. ),
  412. SynapseError,
  413. )
  414. res = e.value.code
  415. self.assertEqual(res, 400)
  416. query_res = self.get_success(
  417. self.handler.query_local_devices({local_user: None})
  418. )
  419. self.assertDictEqual(query_res, {local_user: {}})
  420. def test_upload_signatures(self) -> None:
  421. """should check signatures that are uploaded"""
  422. # set up a user with cross-signing keys and a device. This user will
  423. # try uploading signatures
  424. local_user = "@boris:" + self.hs.hostname
  425. device_id = "xyz"
  426. # private key: OMkooTr76ega06xNvXIGPbgvvxAOzmQncN8VObS7aBA
  427. device_pubkey = "NnHhnqiMFQkq969szYkooLaBAXW244ZOxgukCvm2ZeY"
  428. device_key = {
  429. "user_id": local_user,
  430. "device_id": device_id,
  431. "algorithms": [
  432. "m.olm.curve25519-aes-sha2",
  433. RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
  434. ],
  435. "keys": {"curve25519:xyz": "curve25519+key", "ed25519:xyz": device_pubkey},
  436. "signatures": {local_user: {"ed25519:xyz": "something"}},
  437. }
  438. device_signing_key = key.decode_signing_key_base64(
  439. "ed25519", "xyz", "OMkooTr76ega06xNvXIGPbgvvxAOzmQncN8VObS7aBA"
  440. )
  441. self.get_success(
  442. self.handler.upload_keys_for_user(
  443. local_user, device_id, {"device_keys": device_key}
  444. )
  445. )
  446. # private key: 2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0
  447. master_pubkey = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
  448. master_key = {
  449. "user_id": local_user,
  450. "usage": ["master"],
  451. "keys": {"ed25519:" + master_pubkey: master_pubkey},
  452. }
  453. master_signing_key = key.decode_signing_key_base64(
  454. "ed25519", master_pubkey, "2lonYOM6xYKdEsO+6KrC766xBcHnYnim1x/4LFGF8B0"
  455. )
  456. usersigning_pubkey = "Hq6gL+utB4ET+UvD5ci0kgAwsX6qP/zvf8v6OInU5iw"
  457. usersigning_key = {
  458. # private key: 4TL4AjRYwDVwD3pqQzcor+ez/euOB1/q78aTJ+czDNs
  459. "user_id": local_user,
  460. "usage": ["user_signing"],
  461. "keys": {"ed25519:" + usersigning_pubkey: usersigning_pubkey},
  462. }
  463. usersigning_signing_key = key.decode_signing_key_base64(
  464. "ed25519", usersigning_pubkey, "4TL4AjRYwDVwD3pqQzcor+ez/euOB1/q78aTJ+czDNs"
  465. )
  466. sign.sign_json(usersigning_key, local_user, master_signing_key)
  467. # private key: HvQBbU+hc2Zr+JP1sE0XwBe1pfZZEYtJNPJLZJtS+F8
  468. selfsigning_pubkey = "EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ"
  469. selfsigning_key = {
  470. "user_id": local_user,
  471. "usage": ["self_signing"],
  472. "keys": {"ed25519:" + selfsigning_pubkey: selfsigning_pubkey},
  473. }
  474. selfsigning_signing_key = key.decode_signing_key_base64(
  475. "ed25519", selfsigning_pubkey, "HvQBbU+hc2Zr+JP1sE0XwBe1pfZZEYtJNPJLZJtS+F8"
  476. )
  477. sign.sign_json(selfsigning_key, local_user, master_signing_key)
  478. cross_signing_keys = {
  479. "master_key": master_key,
  480. "user_signing_key": usersigning_key,
  481. "self_signing_key": selfsigning_key,
  482. }
  483. self.get_success(
  484. self.handler.upload_signing_keys_for_user(local_user, cross_signing_keys)
  485. )
  486. # set up another user with a master key. This user will be signed by
  487. # the first user
  488. other_user = "@otherboris:" + self.hs.hostname
  489. other_master_pubkey = "fHZ3NPiKxoLQm5OoZbKa99SYxprOjNs4TwJUKP+twCM"
  490. other_master_key = {
  491. # private key: oyw2ZUx0O4GifbfFYM0nQvj9CL0b8B7cyN4FprtK8OI
  492. "user_id": other_user,
  493. "usage": ["master"],
  494. "keys": {"ed25519:" + other_master_pubkey: other_master_pubkey},
  495. }
  496. self.get_success(
  497. self.handler.upload_signing_keys_for_user(
  498. other_user, {"master_key": other_master_key}
  499. )
  500. )
  501. # test various signature failures (see below)
  502. ret = self.get_success(
  503. self.handler.upload_signatures_for_device_keys(
  504. local_user,
  505. {
  506. local_user: {
  507. # fails because the signature is invalid
  508. # should fail with INVALID_SIGNATURE
  509. device_id: {
  510. "user_id": local_user,
  511. "device_id": device_id,
  512. "algorithms": [
  513. "m.olm.curve25519-aes-sha2",
  514. RoomEncryptionAlgorithms.MEGOLM_V1_AES_SHA2,
  515. ],
  516. "keys": {
  517. "curve25519:xyz": "curve25519+key",
  518. # private key: OMkooTr76ega06xNvXIGPbgvvxAOzmQncN8VObS7aBA
  519. "ed25519:xyz": device_pubkey,
  520. },
  521. "signatures": {
  522. local_user: {
  523. "ed25519:" + selfsigning_pubkey: "something"
  524. }
  525. },
  526. },
  527. # fails because device is unknown
  528. # should fail with NOT_FOUND
  529. "unknown": {
  530. "user_id": local_user,
  531. "device_id": "unknown",
  532. "signatures": {
  533. local_user: {
  534. "ed25519:" + selfsigning_pubkey: "something"
  535. }
  536. },
  537. },
  538. # fails because the signature is invalid
  539. # should fail with INVALID_SIGNATURE
  540. master_pubkey: {
  541. "user_id": local_user,
  542. "usage": ["master"],
  543. "keys": {"ed25519:" + master_pubkey: master_pubkey},
  544. "signatures": {
  545. local_user: {"ed25519:" + device_pubkey: "something"}
  546. },
  547. },
  548. },
  549. other_user: {
  550. # fails because the device is not the user's master-signing key
  551. # should fail with NOT_FOUND
  552. "unknown": {
  553. "user_id": other_user,
  554. "device_id": "unknown",
  555. "signatures": {
  556. local_user: {
  557. "ed25519:" + usersigning_pubkey: "something"
  558. }
  559. },
  560. },
  561. other_master_pubkey: {
  562. # fails because the key doesn't match what the server has
  563. # should fail with UNKNOWN
  564. "user_id": other_user,
  565. "usage": ["master"],
  566. "keys": {
  567. "ed25519:" + other_master_pubkey: other_master_pubkey
  568. },
  569. "something": "random",
  570. "signatures": {
  571. local_user: {
  572. "ed25519:" + usersigning_pubkey: "something"
  573. }
  574. },
  575. },
  576. },
  577. },
  578. )
  579. )
  580. user_failures = ret["failures"][local_user]
  581. self.assertEqual(user_failures[device_id]["errcode"], Codes.INVALID_SIGNATURE)
  582. self.assertEqual(
  583. user_failures[master_pubkey]["errcode"], Codes.INVALID_SIGNATURE
  584. )
  585. self.assertEqual(user_failures["unknown"]["errcode"], Codes.NOT_FOUND)
  586. other_user_failures = ret["failures"][other_user]
  587. self.assertEqual(other_user_failures["unknown"]["errcode"], Codes.NOT_FOUND)
  588. self.assertEqual(
  589. other_user_failures[other_master_pubkey]["errcode"], Codes.UNKNOWN
  590. )
  591. # test successful signatures
  592. del device_key["signatures"]
  593. sign.sign_json(device_key, local_user, selfsigning_signing_key)
  594. sign.sign_json(master_key, local_user, device_signing_key)
  595. sign.sign_json(other_master_key, local_user, usersigning_signing_key)
  596. ret = self.get_success(
  597. self.handler.upload_signatures_for_device_keys(
  598. local_user,
  599. {
  600. local_user: {device_id: device_key, master_pubkey: master_key},
  601. other_user: {other_master_pubkey: other_master_key},
  602. },
  603. )
  604. )
  605. self.assertEqual(ret["failures"], {})
  606. # fetch the signed keys/devices and make sure that the signatures are there
  607. ret = self.get_success(
  608. self.handler.query_devices(
  609. {"device_keys": {local_user: [], other_user: []}},
  610. 0,
  611. local_user,
  612. "device123",
  613. )
  614. )
  615. self.assertEqual(
  616. ret["device_keys"][local_user]["xyz"]["signatures"][local_user][
  617. "ed25519:" + selfsigning_pubkey
  618. ],
  619. device_key["signatures"][local_user]["ed25519:" + selfsigning_pubkey],
  620. )
  621. self.assertEqual(
  622. ret["master_keys"][local_user]["signatures"][local_user][
  623. "ed25519:" + device_id
  624. ],
  625. master_key["signatures"][local_user]["ed25519:" + device_id],
  626. )
  627. self.assertEqual(
  628. ret["master_keys"][other_user]["signatures"][local_user][
  629. "ed25519:" + usersigning_pubkey
  630. ],
  631. other_master_key["signatures"][local_user]["ed25519:" + usersigning_pubkey],
  632. )
  633. def test_query_devices_remote_no_sync(self) -> None:
  634. """Tests that querying keys for a remote user that we don't share a room
  635. with returns the cross signing keys correctly.
  636. """
  637. remote_user_id = "@test:other"
  638. local_user_id = "@test:test"
  639. remote_master_key = "85T7JXPFBAySB/jwby4S3lBPTqY3+Zg53nYuGmu1ggY"
  640. remote_self_signing_key = "QeIiFEjluPBtI7WQdG365QKZcFs9kqmHir6RBD0//nQ"
  641. self.hs.get_federation_client().query_client_keys = mock.Mock(
  642. return_value=make_awaitable(
  643. {
  644. "device_keys": {remote_user_id: {}},
  645. "master_keys": {
  646. remote_user_id: {
  647. "user_id": remote_user_id,
  648. "usage": ["master"],
  649. "keys": {"ed25519:" + remote_master_key: remote_master_key},
  650. },
  651. },
  652. "self_signing_keys": {
  653. remote_user_id: {
  654. "user_id": remote_user_id,
  655. "usage": ["self_signing"],
  656. "keys": {
  657. "ed25519:"
  658. + remote_self_signing_key: remote_self_signing_key
  659. },
  660. }
  661. },
  662. }
  663. )
  664. )
  665. e2e_handler = self.hs.get_e2e_keys_handler()
  666. query_result = self.get_success(
  667. e2e_handler.query_devices(
  668. {
  669. "device_keys": {remote_user_id: []},
  670. },
  671. timeout=10,
  672. from_user_id=local_user_id,
  673. from_device_id="some_device_id",
  674. )
  675. )
  676. self.assertEqual(query_result["failures"], {})
  677. self.assertEqual(
  678. query_result["master_keys"],
  679. {
  680. remote_user_id: {
  681. "user_id": remote_user_id,
  682. "usage": ["master"],
  683. "keys": {"ed25519:" + remote_master_key: remote_master_key},
  684. },
  685. },
  686. )
  687. self.assertEqual(
  688. query_result["self_signing_keys"],
  689. {
  690. remote_user_id: {
  691. "user_id": remote_user_id,
  692. "usage": ["self_signing"],
  693. "keys": {
  694. "ed25519:" + remote_self_signing_key: remote_self_signing_key
  695. },
  696. }
  697. },
  698. )
  699. def test_query_devices_remote_sync(self) -> None:
  700. """Tests that querying keys for a remote user that we share a room with,
  701. but haven't yet fetched the keys for, returns the cross signing keys
  702. correctly.
  703. """
  704. remote_user_id = "@test:other"
  705. local_user_id = "@test:test"
  706. # Pretend we're sharing a room with the user we're querying. If not,
  707. # `_query_devices_for_destination` will return early.
  708. self.store.get_rooms_for_user = mock.Mock(
  709. return_value=make_awaitable({"some_room_id"})
  710. )
  711. remote_master_key = "85T7JXPFBAySB/jwby4S3lBPTqY3+Zg53nYuGmu1ggY"
  712. remote_self_signing_key = "QeIiFEjluPBtI7WQdG365QKZcFs9kqmHir6RBD0//nQ"
  713. self.hs.get_federation_client().query_user_devices = mock.Mock(
  714. return_value=make_awaitable(
  715. {
  716. "user_id": remote_user_id,
  717. "stream_id": 1,
  718. "devices": [],
  719. "master_key": {
  720. "user_id": remote_user_id,
  721. "usage": ["master"],
  722. "keys": {"ed25519:" + remote_master_key: remote_master_key},
  723. },
  724. "self_signing_key": {
  725. "user_id": remote_user_id,
  726. "usage": ["self_signing"],
  727. "keys": {
  728. "ed25519:"
  729. + remote_self_signing_key: remote_self_signing_key
  730. },
  731. },
  732. }
  733. )
  734. )
  735. e2e_handler = self.hs.get_e2e_keys_handler()
  736. query_result = self.get_success(
  737. e2e_handler.query_devices(
  738. {
  739. "device_keys": {remote_user_id: []},
  740. },
  741. timeout=10,
  742. from_user_id=local_user_id,
  743. from_device_id="some_device_id",
  744. )
  745. )
  746. self.assertEqual(query_result["failures"], {})
  747. self.assertEqual(
  748. query_result["master_keys"],
  749. {
  750. remote_user_id: {
  751. "user_id": remote_user_id,
  752. "usage": ["master"],
  753. "keys": {"ed25519:" + remote_master_key: remote_master_key},
  754. }
  755. },
  756. )
  757. self.assertEqual(
  758. query_result["self_signing_keys"],
  759. {
  760. remote_user_id: {
  761. "user_id": remote_user_id,
  762. "usage": ["self_signing"],
  763. "keys": {
  764. "ed25519:" + remote_self_signing_key: remote_self_signing_key
  765. },
  766. }
  767. },
  768. )
  769. @parameterized.expand(
  770. [
  771. # The remote homeserver's response indicates that this user has 0/1/2 devices.
  772. ([],),
  773. (["device_1"],),
  774. (["device_1", "device_2"],),
  775. ]
  776. )
  777. def test_query_all_devices_caches_result(self, device_ids: Iterable[str]) -> None:
  778. """Test that requests for all of a remote user's devices are cached.
  779. We do this by asserting that only one call over federation was made, and that
  780. the two queries to the local homeserver produce the same response.
  781. """
  782. local_user_id = "@test:test"
  783. remote_user_id = "@test:other"
  784. request_body: JsonDict = {"device_keys": {remote_user_id: []}}
  785. response_devices = [
  786. {
  787. "device_id": device_id,
  788. "keys": {
  789. "algorithms": ["dummy"],
  790. "device_id": device_id,
  791. "keys": {f"dummy:{device_id}": "dummy"},
  792. "signatures": {device_id: {f"dummy:{device_id}": "dummy"}},
  793. "unsigned": {},
  794. "user_id": "@test:other",
  795. },
  796. }
  797. for device_id in device_ids
  798. ]
  799. response_body = {
  800. "devices": response_devices,
  801. "user_id": remote_user_id,
  802. "stream_id": 12345, # an integer, according to the spec
  803. }
  804. e2e_handler = self.hs.get_e2e_keys_handler()
  805. # Pretend we're sharing a room with the user we're querying. If not,
  806. # `_query_devices_for_destination` will return early.
  807. mock_get_rooms = mock.patch.object(
  808. self.store,
  809. "get_rooms_for_user",
  810. new_callable=mock.MagicMock,
  811. return_value=make_awaitable(["some_room_id"]),
  812. )
  813. mock_request = mock.patch.object(
  814. self.hs.get_federation_client(),
  815. "query_user_devices",
  816. new_callable=mock.MagicMock,
  817. return_value=make_awaitable(response_body),
  818. )
  819. with mock_get_rooms, mock_request as mocked_federation_request:
  820. # Make the first query and sanity check it succeeds.
  821. response_1 = self.get_success(
  822. e2e_handler.query_devices(
  823. request_body,
  824. timeout=10,
  825. from_user_id=local_user_id,
  826. from_device_id="some_device_id",
  827. )
  828. )
  829. self.assertEqual(response_1["failures"], {})
  830. # We should have made a federation request to do so.
  831. mocked_federation_request.assert_called_once()
  832. # Reset the mock so we can prove we don't make a second federation request.
  833. mocked_federation_request.reset_mock()
  834. # Repeat the query.
  835. response_2 = self.get_success(
  836. e2e_handler.query_devices(
  837. request_body,
  838. timeout=10,
  839. from_user_id=local_user_id,
  840. from_device_id="some_device_id",
  841. )
  842. )
  843. self.assertEqual(response_2["failures"], {})
  844. # We should not have made a second federation request.
  845. mocked_federation_request.assert_not_called()
  846. # The two requests to the local homeserver should be identical.
  847. self.assertEqual(response_1, response_2)